Skip to main content

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}