Skip to main content

swls_core/components/
config.rs

1use std::{collections::HashSet, path::PathBuf};
2
3use bevy_ecs::prelude::*;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    lsp_types::{Url, WorkspaceFolder},
8    util::fs::Fs,
9};
10
11#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub enum Disabled {
13    #[serde(alias = "SHAPES", alias = "shapes")]
14    Shapes,
15
16    // --- diagnostics ---
17    /// Diagnostic for predicate/object IRIs that use an undeclared prefix.
18    #[serde(alias = "UNDEFINED_PREFIX", alias = "undefined_prefix")]
19    UndefinedPrefix,
20    /// Diagnostic for prefixes that are declared but never used.
21    #[serde(alias = "UNUSED_PREFIX", alias = "unused_prefix")]
22    UnusedPrefix,
23    /// Diagnostic for properties under a `closed_namespaces` namespace that are
24    /// not known to the ontology and not in `allowed_properties`.
25    #[serde(alias = "NAMESPACE_PROPERTIES", alias = "namespace_properties")]
26    NamespaceProperties,
27    /// Diagnostic for syntax/parse errors.
28    #[serde(alias = "SYNTAX_DIAGNOSTICS", alias = "syntax_diagnostics")]
29    SyntaxDiagnostics,
30
31    // --- LSP features ---
32    /// Master switch: disables `textDocument/completion` entirely (and stops
33    /// advertising the capability). See also the `completion_*` variants below
34    /// to disable individual completion sources while keeping completion on.
35    #[serde(alias = "COMPLETION", alias = "completion")]
36    Completion,
37    /// Keyword completion (e.g. `@prefix`, `@context`).
38    #[serde(alias = "COMPLETION_KEYWORD", alias = "completion_keyword")]
39    CompletionKeyword,
40    /// RDF class-name completion (e.g. after `a `/`rdf:type`).
41    #[serde(alias = "COMPLETION_CLASS", alias = "completion_class")]
42    CompletionClass,
43    /// RDF property/predicate-name completion.
44    #[serde(alias = "COMPLETION_PROPERTY", alias = "completion_property")]
45    CompletionProperty,
46    /// Prefix-name completion sourced from bundled LOV / prefix.cc data (also
47    /// inserts the matching declaration).
48    #[serde(alias = "COMPLETION_PREFIX", alias = "completion_prefix")]
49    CompletionPrefix,
50    /// Subject-IRI completion that reuses subjects already used in the document
51    /// (Turtle only).
52    #[serde(alias = "COMPLETION_SUBJECT", alias = "completion_subject")]
53    CompletionSubject,
54
55    /// Master switch: disables `textDocument/hover` entirely (and stops
56    /// advertising the capability). See also the `hover_*` variants below to
57    /// disable individual hover sources while keeping hover on.
58    #[serde(alias = "HOVER", alias = "hover")]
59    Hover,
60    /// Hover showing the inferred RDF type(s) of the term under the cursor.
61    #[serde(alias = "HOVER_TYPE", alias = "hover_type")]
62    HoverType,
63    /// Hover showing ontology documentation for an RDF class IRI.
64    #[serde(alias = "HOVER_CLASS", alias = "hover_class")]
65    HoverClass,
66    /// Hover showing ontology documentation for a property/predicate IRI.
67    #[serde(alias = "HOVER_PROPERTY", alias = "hover_property")]
68    HoverProperty,
69    /// Hover explanation shown for a property that is only accepted because it
70    /// is in the user's `allowed_properties` allow-list.
71    #[serde(
72        alias = "HOVER_EXCLUDED_PROPERTY",
73        alias = "hover_excluded_property"
74    )]
75    HoverExcludedProperty,
76
77    /// Master switch: disables `textDocument/definition` entirely (and stops
78    /// advertising the capability). Covers generic RDF term goto-definition.
79    #[serde(alias = "GOTO_DEFINITION", alias = "goto_definition")]
80    GotoDefinition,
81    /// Components.js-specific goto-definition: resolves component/module/
82    /// parameter IRIs and import/context URLs to their source file.
83    #[serde(
84        alias = "GOTO_DEFINITION_COMPONENTS_JS",
85        alias = "goto_definition_components_js"
86    )]
87    GotoDefinitionComponentsJs,
88    #[serde(alias = "GOTO_TYPE_DEFINITION", alias = "goto_type_definition")]
89    GotoTypeDefinition,
90    #[serde(alias = "REFERENCES", alias = "references")]
91    References,
92    #[serde(alias = "RENAME", alias = "rename")]
93    Rename,
94    #[serde(alias = "SEMANTIC_TOKENS", alias = "semantic_tokens")]
95    SemanticTokens,
96    #[serde(alias = "FORMAT", alias = "format")]
97    Format,
98    /// Auto-inserts the missing prefix/context declaration while typing
99    /// `prefix:` (formerly named `on_type_format`).
100    #[serde(
101        alias = "PREFIX_AUTO_INSERT",
102        alias = "prefix_auto_insert",
103        alias = "ON_TYPE_FORMAT",
104        alias = "on_type_format"
105    )]
106    PrefixAutoInsert,
107    /// Master switch: disables `textDocument/codeAction` entirely (and stops
108    /// advertising the capability). The "add missing prefix" and "allow
109    /// property" quick-fixes are controlled by their respective diagnostic
110    /// toggles ([`Disabled::UndefinedPrefix`], [`Disabled::NamespaceProperties`])
111    /// instead, since they only make sense alongside their diagnostic.
112    #[serde(alias = "CODE_ACTION", alias = "code_action")]
113    CodeAction,
114    /// "Organize Imports" quick-fix that sorts `@prefix` declarations (Turtle).
115    #[serde(
116        alias = "CODE_ACTION_ORGANIZE_IMPORTS",
117        alias = "code_action_organize_imports"
118    )]
119    CodeActionOrganizeImports,
120    /// "Extract blank node" / "Inline named blank node" quick-fixes.
121    #[serde(
122        alias = "CODE_ACTION_BLANK_NODE_REFACTOR",
123        alias = "code_action_blank_node_refactor"
124    )]
125    CodeActionBlankNodeRefactor,
126    #[serde(alias = "INLAY_HINT", alias = "inlay_hint")]
127    InlayHint,
128}
129
130/// How Turtle/TriG prefix declarations should be written when the editor inserts
131/// them (e.g. during prefix completion or the "add missing prefix" quick-fix).
132///
133/// Turtle 1.1 allows both the classic `@prefix ex: <...> .` form and the
134/// SPARQL-style `PREFIX ex: <...>` form (no trailing dot).
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
136#[serde(rename_all = "lowercase")]
137pub enum PrefixFormat {
138    /// `@prefix ex: <...> .`
139    #[default]
140    Turtle,
141    /// `PREFIX ex: <...>`
142    Sparql,
143}
144
145#[derive(Resource, Debug, Default)]
146pub struct ServerConfig {
147    pub workspaces: Vec<WorkspaceFolder>,
148    pub config: Config,
149}
150
151#[derive(Debug, Deserialize)]
152pub struct Config {
153    /// Log level
154    #[serde(default = "debug")]
155    pub log: String,
156    /// Enable turtle
157    pub turtle: Option<bool>,
158    /// Enable trig
159    pub trig: Option<bool>,
160    /// Enable n3 (opt-in: disabled unless explicitly set to `true`)
161    pub n3: Option<bool>,
162    /// Enable jsonld
163    pub jsonld: Option<bool>,
164    /// Enable sparql
165    pub sparql: Option<bool>,
166    /// Per-language `textDocument/formatting` toggles. Formatting is disabled by
167    /// default for every language except Turtle.
168    #[serde(default)]
169    pub format: FormatConfig,
170    /// Extra local configuration
171    #[serde(flatten)]
172    pub local: LocalConfig,
173}
174
175/// Per-language toggles for `textDocument/formatting`.
176///
177/// Each field is an `Option<bool>`, but the *effective* default differs per
178/// language: Turtle formatting is on unless explicitly disabled, while every
179/// other language is off unless explicitly enabled. Use the accessor methods
180/// ([`FormatConfig::turtle`] etc.) to resolve a field to its effective value
181/// rather than reading the `Option` directly.
182#[derive(Debug, Deserialize, Serialize, Default)]
183#[serde(default)]
184pub struct FormatConfig {
185    /// Enable Turtle formatting (default: `true`).
186    pub turtle: Option<bool>,
187    /// Enable TriG formatting (default: `false`).
188    pub trig: Option<bool>,
189    /// Enable N3 formatting (default: `false`).
190    pub n3: Option<bool>,
191    /// Enable JSON-LD formatting (default: `false`).
192    pub jsonld: Option<bool>,
193}
194
195impl FormatConfig {
196    /// Whether Turtle formatting is enabled (defaults to `true`).
197    pub fn turtle(&self) -> bool {
198        self.turtle.unwrap_or(true)
199    }
200    /// Whether TriG formatting is enabled (defaults to `false`).
201    pub fn trig(&self) -> bool {
202        self.trig.unwrap_or(false)
203    }
204    /// Whether N3 formatting is enabled (defaults to `false`).
205    pub fn n3(&self) -> bool {
206        self.n3.unwrap_or(false)
207    }
208    /// Whether JSON-LD formatting is enabled (defaults to `false`).
209    pub fn jsonld(&self) -> bool {
210        self.jsonld.unwrap_or(false)
211    }
212}
213
214#[derive(Debug, Deserialize, Serialize, Default)]
215#[serde(default)]
216pub struct LocalConfig {
217    /// Extra ontologies to import
218    pub ontologies: HashSet<String>,
219    /// Extra shapes to import
220    pub shapes: HashSet<String>,
221    /// Features to disable
222    pub disabled: HashSet<Disabled>,
223    /// disable which prefices from prefix.cc to show
224    pub prefix_disabled: HashSet<String>,
225    /// confiure completion behavior
226    pub completion: CompletionConfig,
227    /// Preferred way to write Turtle/TriG prefix declarations when inserting them.
228    pub prefix_format: Option<PrefixFormat>,
229    /// Namespaces for which IRIs used as properties (predicates) must be defined in a
230    /// known ontology. Predicate IRIs that start with one of these namespaces but are
231    /// not a known property (and not in [`allowed_properties`]) are flagged with a warning.
232    pub closed_namespaces: HashSet<String>,
233    /// User-approved property IRIs that should not be flagged by the
234    /// [`closed_namespaces`] validation, even though they are absent from the ontology.
235    pub allowed_properties: HashSet<String>,
236}
237
238/// Lets the user configure how the property completion should happen.
239/// There are two main modes: strict and loose (default)
240/// On loose, the editor will suggest anything, not caring about the domain.
241/// On strict, the editor will only suggest property that have a matching domain or anything if the
242/// type could not be determined.
243///
244/// Both options can be specialized, ie only strict on these properties or only loose on these
245/// properties.
246///
247/// For example { loose: ["http://www.w3.org/2000/01/rdf-schema#"] }, here the editor will be strict, and show properties
248/// from rdfs
249/// On the other hand { strict: ["http://www.w3.org/ns/shacl#"] }, here the editor will be loose,
250/// and only show shacl properties if the objects is the correct type
251#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
252#[serde(untagged)]
253pub enum CompletionConfig {
254    // "strict" | "loose" | "none"
255    Mode(CompletionMode),
256
257    // { "except": [...] }
258    Except(ExceptRules),
259
260    // { "strict": [...] }
261    Strict(StrictRules),
262}
263
264impl Default for CompletionConfig {
265    fn default() -> Self {
266        Self::Mode(CompletionMode::None)
267    }
268}
269
270impl CompletionConfig {
271    fn combine(&mut self, other: CompletionConfig) {
272        use CompletionConfig::*;
273
274        if matches!(other, Mode(CompletionMode::None)) {
275            return;
276        }
277
278        if matches!(self, Mode(CompletionMode::None)) {
279            *self = other;
280            return;
281        }
282
283        if let Strict(r) = self {
284            if let Strict(r2) = other {
285                r.strict.extend(r2.strict);
286                return;
287            }
288        }
289
290        if let Except(r) = self {
291            if let Except(r2) = other {
292                r.loose.extend(r2.loose);
293                return;
294            }
295        }
296
297        *self = other;
298    }
299    pub fn correct_domain_required(&self, property: &str) -> bool {
300        match self {
301            CompletionConfig::Mode(CompletionMode::Loose)
302            | CompletionConfig::Mode(CompletionMode::None) => false,
303            CompletionConfig::Mode(CompletionMode::Strict) => true,
304            CompletionConfig::Except(completion_rules) => !completion_rules
305                .loose
306                .iter()
307                .any(|x| property.starts_with(x)),
308            CompletionConfig::Strict(completion_rules) => completion_rules
309                .strict
310                .iter()
311                .any(|x| property.starts_with(x)),
312        }
313    }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
317#[serde(rename_all = "lowercase")]
318pub enum CompletionMode {
319    #[default]
320    None,
321    Loose,
322    Strict,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
326#[serde(deny_unknown_fields)]
327pub struct ExceptRules {
328    #[serde(default)]
329    pub loose: Vec<String>,
330}
331#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
332#[serde(deny_unknown_fields)]
333pub struct StrictRules {
334    #[serde(default)]
335    pub strict: Vec<String>,
336}
337
338impl LocalConfig {
339    /// Whether the given feature/diagnostic has been disabled by the user.
340    pub fn is_disabled(&self, d: Disabled) -> bool {
341        self.disabled.contains(&d)
342    }
343
344    /// Combines this config with another config, giving precedence to the other config
345    pub fn combine(&mut self, other: LocalConfig) {
346        self.ontologies.extend(other.ontologies);
347        self.shapes.extend(other.shapes);
348        self.disabled.extend(other.disabled);
349        self.prefix_disabled.extend(other.prefix_disabled);
350        self.completion.combine(other.completion);
351        if other.prefix_format.is_some() {
352            self.prefix_format = other.prefix_format;
353        }
354        self.closed_namespaces.extend(other.closed_namespaces);
355        self.allowed_properties.extend(other.allowed_properties);
356    }
357    #[cfg(target_arch = "wasm32")]
358    pub async fn global(_: &Fs) -> Option<Self> {
359        None
360    }
361
362    #[cfg(not(target_arch = "wasm32"))]
363    pub fn global_url() -> Option<Url> {
364        let global_path = dirs::config_dir()
365            .unwrap_or_else(|| PathBuf::from("."))
366            .join("swls/config.json");
367        crate::lsp_types::Url::from_file_path(global_path).ok()
368    }
369
370    #[cfg(target_arch = "wasm32")]
371    pub fn global_url() -> Option<Url> {
372        None
373    }
374
375    #[cfg(not(target_arch = "wasm32"))]
376    pub async fn global(fs: &Fs) -> Option<Self> {
377        let url = Self::global_url()?;
378
379        tracing::debug!("Found global config url {}", url.as_str());
380        let content = fs.0.read_file(&url).await?;
381        tracing::debug!("Read global config content");
382
383        match serde_json::from_str(&content) {
384            Ok(x) => Some(x),
385            Err(e) => {
386                tracing::error!("Deserialize failed\n{:?}", e);
387                None
388            }
389        }
390    }
391
392    /// Add `iri` to the global config's `allowed_properties` array on disk,
393    /// preserving the rest of the existing global configuration.  Used by the
394    /// `swls.allowProperty` command so the user's choice survives restarts.
395    #[cfg(not(target_arch = "wasm32"))]
396    pub async fn persist_allowed_property(fs: &Fs, iri: &str) -> Option<()> {
397        let url = Self::global_url()?;
398        let mut existing = Self::global(fs).await.unwrap_or_default();
399        existing.allowed_properties.insert(iri.to_string());
400        let content = serde_json::to_string_pretty(&existing).ok()?;
401        fs.0.write_file(&url, &content).await
402    }
403
404    #[cfg(target_arch = "wasm32")]
405    pub async fn persist_allowed_property(_: &Fs, _: &str) -> Option<()> {
406        None
407    }
408
409    pub async fn local(fs: &Fs, url: &Url) -> Option<Self> {
410        let url = Url::parse(&format!("{}/.swls/config.json", url.as_str())).ok()?;
411        tracing::debug!("Found local config url {}", url.as_str());
412        let content = fs.0.read_file(&url).await?;
413        tracing::debug!("Read local config content");
414        match serde_json::from_str(&content) {
415            Ok(x) => Some(x),
416            Err(e) => {
417                tracing::error!("Deserialize failed\n{:?}", e);
418                None
419            }
420        }
421    }
422}
423
424impl Default for Config {
425    fn default() -> Self {
426        Self {
427            log: "debug".to_string(),
428            turtle: None,
429            trig: None,
430            n3: None,
431            jsonld: None,
432            sparql: None,
433            format: FormatConfig::default(),
434            local: LocalConfig::default(),
435        }
436    }
437}
438
439fn debug() -> String {
440    String::from("debug")
441}