swls_core/lang.rs
1use std::{borrow::Cow, ops::Range};
2
3use crate::{
4 lsp_types::SemanticTokenType, prelude::TripleTarget, text::LineIndex, util::offset_to_position,
5};
6
7pub fn head() -> crate::lsp_types::Range {
8 let start = crate::lsp_types::Position {
9 line: 0,
10 character: 0,
11 };
12 crate::lsp_types::Range {
13 end: start.clone(),
14 start,
15 }
16}
17
18pub trait Lang: 'static {
19 type ElementError: Into<crate::feature::diagnostics::SimpleDiagnostic>
20 + Send
21 + Sync
22 + std::fmt::Debug;
23
24 const CODE_ACTION: bool;
25 const HOVER: bool;
26 const LANG: &'static str;
27 const TRIGGERS: &'static [&'static str];
28 const LEGEND_TYPES: &'static [SemanticTokenType];
29 const PATTERN: Option<&'static str>;
30
31 /// Map a CST syntax kind to a semantic token type for highlighting.
32 fn semantic_token_type(_kind: rowan::SyntaxKind) -> Option<SemanticTokenType> {
33 None
34 }
35
36 /// Map a CST syntax kind + byte span to semantic token spans for highlighting.
37 fn semantic_token_spans(
38 kind: rowan::SyntaxKind,
39 span: std::ops::Range<usize>,
40 _text: &str,
41 ) -> Vec<(SemanticTokenType, std::ops::Range<usize>)> {
42 Self::semantic_token_type(kind)
43 .map(|t| vec![(t, span)])
44 .unwrap_or_default()
45 }
46}
47
48pub trait LangHelper: std::fmt::Debug {
49 fn keyword(&self) -> &[&'static str];
50 fn default_position(&self) -> TripleTarget {
51 TripleTarget::Subject
52 }
53 fn unquote<'a>(&self, inp: &'a str) -> &'a str {
54 inp
55 }
56 fn quote(&self, inp: &str) -> String {
57 format!("{}", inp)
58 }
59 /// Return the source keyword used to introduce a prefix declaration
60 /// (used only for display / span-finding purposes).
61 fn prefix_keyword(&self) -> &str {
62 "@prefix"
63 }
64 /// Format a prefix declaration string to be inserted into a document.
65 ///
66 /// Default (Turtle / TriG): honors the user's [`PrefixFormat`] preference —
67 /// `@prefix {name}: <{url}>.\n` (Turtle) or `PREFIX {name}: <{url}>\n` (SPARQL-style).
68 fn format_prefix_declaration(
69 &self,
70 name: &str,
71 url: &str,
72 format: crate::components::PrefixFormat,
73 ) -> String {
74 match format {
75 crate::components::PrefixFormat::Sparql => format!("PREFIX {}: <{}>\n", name, url),
76 crate::components::PrefixFormat::Turtle => format!("@prefix {}: <{}>.\n", name, url),
77 }
78 }
79 /// Produce the text edit(s) that declare prefix `name` → `namespace` in this
80 /// document, or `None` when it cannot / should not be inserted (e.g. the
81 /// prefix is already present).
82 ///
83 /// This is the single source of truth for "how do I add a prefix to a
84 /// document of this language", shared by prefix completion and the
85 /// "add missing prefix" diagnostic quick-fix so the two paths stay
86 /// consistent. The default (Turtle / SPARQL / TriG) inserts a declaration
87 /// line at the very top of the file; JSON-LD overrides this to splice the
88 /// prefix into the `@context` object.
89 fn prefix_edits(
90 &self,
91 _source: &str,
92 _rope: &LineIndex,
93 name: &str,
94 namespace: &str,
95 format: crate::components::PrefixFormat,
96 ) -> Option<Vec<crate::lsp_types::TextEdit>> {
97 let pos = crate::lsp_types::Position::new(0, 0);
98 Some(vec![crate::lsp_types::TextEdit {
99 range: crate::lsp_types::Range::new(pos, pos),
100 new_text: self.format_prefix_declaration(name, namespace, format),
101 }])
102 }
103 /// Return `true` if the generic prefix-diagnostics system should analyse this
104 /// language's documents.
105 ///
106 /// JSON-LD uses `@context` semantics where terms are pre-expanded before being
107 /// stored as `Triples`; its prefix model is not compatible with the span-based
108 /// detection used by the generic system. Override to return `false` to opt out.
109 fn supports_prefix_diagnostics(&self) -> bool {
110 true
111 }
112 /// Extract the prefix name of a prefixed term whose `:` was just typed at byte
113 /// `offset` (the cursor sits immediately after the `:`), or `None` when the
114 /// context is not a bare prefixed name. Used by on-type formatting to decide
115 /// whether to auto-declare a prefix.
116 ///
117 /// Default (Turtle / TriG / SPARQL): scan back over prefix-name characters and
118 /// require a term boundary before them, skipping comments and
119 /// `@prefix`/`@base` (or `PREFIX`/`BASE`) declaration lines. JSON-LD overrides
120 /// this to scan inside a JSON string instead.
121 fn prefix_name_at<'a>(&self, source: &'a str, offset: usize) -> Option<&'a str> {
122 let bytes = source.as_bytes();
123 if offset == 0 || offset > source.len() || bytes[offset - 1] != b':' {
124 return None;
125 }
126 let colon = offset - 1;
127
128 let mut start = colon;
129 while start > 0 {
130 let c = bytes[start - 1];
131 if c.is_ascii_alphanumeric() || matches!(c, b'_' | b'-' | b'.') {
132 start -= 1;
133 } else {
134 break;
135 }
136 }
137 if start == colon {
138 return None;
139 }
140
141 // The character before the name must be a term boundary; keeps us from
142 // matching `:` inside an IRI (`<…:…>`), a string literal, or a pname.
143 let boundary_ok = start == 0
144 || matches!(
145 bytes[start - 1],
146 b' ' | b'\t' | b'\n' | b'\r' | b';' | b',' | b'[' | b'(' | b'{'
147 );
148 if !boundary_ok {
149 return None;
150 }
151
152 // Skip comments and prefix/base declaration lines.
153 let line_start = source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
154 let before = &source[line_start..start];
155 if before.contains('#') {
156 return None;
157 }
158 let keyword = before.trim();
159 if keyword.eq_ignore_ascii_case("@prefix")
160 || keyword.eq_ignore_ascii_case("prefix")
161 || keyword.eq_ignore_ascii_case("@base")
162 || keyword.eq_ignore_ascii_case("base")
163 {
164 return None;
165 }
166
167 Some(&source[start..colon])
168 }
169 /// Given a raw token string from the document (e.g. `<http://ex.org/foo>` or `ex:foo`),
170 /// return the bare text that should be pre-filled in the editor's rename input box.
171 fn rename_placeholder<'a>(&self, raw: &'a str) -> &'a str {
172 // Default (Turtle / SPARQL / TriG): strip surrounding `< >`
173 let s = raw.strip_prefix('<').unwrap_or(raw);
174 s.strip_suffix('>').unwrap_or(s)
175 }
176 /// Wrap the user-supplied rename text so that it is valid in the current language.
177 ///
178 /// Default (Turtle / SPARQL / TriG) smart rules:
179 /// - already has `< >` → keep as-is
180 /// - starts with `_:` → blank node, keep as-is
181 /// - contains `://` → full IRI with scheme (e.g. `http://`), wrap in `< >`
182 /// - contains `:` but no `://` → prefixed name (e.g. `ex:foo`), keep as-is
183 /// - otherwise → bare label, wrap in `< >` to be safe
184 fn rename_wrap(&self, new_text: &str) -> String {
185 if new_text.starts_with('<') && new_text.ends_with('>') {
186 new_text.to_string()
187 } else if new_text.starts_with("_:") {
188 new_text.to_string()
189 } else if new_text.contains("://") {
190 format!("<{}>", new_text)
191 } else if new_text.contains(':') {
192 // Prefixed name like ex:foo
193 new_text.to_string()
194 } else {
195 format!("<{}>", new_text)
196 }
197 }
198 /// Return `true` if this language provides its own prefix completion and
199 /// the generic [`defined_prefix_completion`] system should be skipped.
200 fn handles_prefix_completion(&self) -> bool {
201 false
202 }
203 /// Return `true` if this language renames via the model-based systems in
204 /// `swls-lang-rdf-base` (registered through `setup_rename`). When `true`,
205 /// the core language-agnostic `prepare_rename`/`rename` systems skip this
206 /// language's documents to avoid producing duplicate edits.
207 ///
208 /// Text RDF syntaxes (Turtle / TriG / SPARQL) override this to `true`;
209 /// JSON-LD keeps the default and stays on the agnostic path.
210 fn model_based_rename(&self) -> bool {
211 false
212 }
213 /// Return `true` if this language supports the generic blank-node refactor
214 /// code actions (extract a nested `[ … ]` to a labelled `_:bN`, and inline a
215 /// labelled `_:bN` back into `[ … ]`).
216 ///
217 /// These actions emit Turtle-family syntax, so the text RDF syntaxes
218 /// (Turtle / TriG / SPARQL / N3) override this to `true`. JSON-LD keeps the
219 /// default because its blank nodes are JSON objects, not `[ … ]` / `_:bN`.
220 fn blank_node_code_actions(&self) -> bool {
221 false
222 }
223 fn supports_shape_validation(&self) -> bool {
224 true
225 }
226
227 fn inlay_types_hint(
228 &self,
229 subject: &Range<usize>,
230 rope: &LineIndex,
231 last_type: Option<&Range<usize>>,
232 types: Vec<Cow<'_, str>>,
233 ) -> Option<crate::lsp_types::InlayHint> {
234 let (label, position) = if let Some(lt) = last_type {
235 if let Some(pos) = offset_to_position(lt.end, &rope) {
236 let label = format!(", {}", types.join(", "));
237 (label, pos)
238 } else {
239 return None;
240 }
241 } else {
242 let offset = if rope.char_at_byte(subject.start) == Some('[') {
243 subject.start + 1
244 } else {
245 subject.end
246 };
247
248 if let Some(pos) = offset_to_position(offset, &rope) {
249 let label = format!(" a {};", types.join(", "));
250 (label, pos)
251 } else {
252 return None;
253 }
254 };
255
256 return Some(crate::lsp_types::InlayHint {
257 position,
258 label: crate::lsp_types::InlayHintLabel::String(label),
259 kind: None,
260 text_edits: None,
261 tooltip: None,
262 padding_left: None,
263 padding_right: None,
264 data: None,
265 });
266 }
267}