Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ derive_more = { version = "2", features = ["as_ref", "deref", "deref_mut", "is_v
futures = { version = "0.3.31", features = ["alloc", "executor"], default-features = false }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
# rdf-parsers = { path = "../rdf-parsers/" }
rdf-parsers = { version = "0.1.16" }
rdf-parsers = { version = "0.2.0" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.145"
sophia_api = "0.9.0"
Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Details and caveats for each editor are in [Installation](#installation) below.
| **Hover** | Inferred RDF type · class & property documentation from the ontology · explanation when a property is only accepted via your allow-list |
| **Navigation** | Go to definition (RDF terms and, for JSON-LD, Components.js modules/parameters) · go to type definition · find references · rename |
| **Code actions** | Add missing prefix declaration · allow-list an unknown property · organize/sort `@prefix` imports (Turtle) · extract/inline a blank node |
| **Formatting** | Document formatting for Turtle and JSON-LD · auto-insert the prefix declaration while typing |
| **Formatting** | Document formatting for Turtle, TriG, SPARQL, N3 and JSON-LD · auto-insert the prefix declaration while typing |
| **Highlighting** | Semantic syntax highlighting |
| **Inlay hints** | Inferred type shown inline next to subjects missing an explicit `rdf:type` |

Expand Down Expand Up @@ -68,19 +68,31 @@ point your editor's LSP client at it for `.ttl`, `.trig`, `.jsonld` and `.sparql
## Configuration

SWLS reads configuration from the client's `initializationOptions`, plus optional
`.swls/config.json` (workspace) and `~/.config/swls/config.json` (global) files.
`~/.config/swls/config.json` (global) and `.swls/config.json` (workspace) files. Later sources
win: a workspace file overrides the global one, which overrides `initializationOptions`.

```json
{
"turtle": true,
"sparql": false,
"disabled": ["unused_prefix", "hover_excluded_property"]
"disabled": ["unused_prefix", "hover_excluded_property"],
"format": { "jsonld": false },
"prefix_format": "sparql"
}
```

- `turtle` / `trig` / `jsonld` / `sparql` (default `true`) — enable/disable a language plugin entirely.
These are only read from `initializationOptions`.
- `disabled` — a list of individual diagnostics or LSP (sub-)features to turn off, e.g. just the
"unused prefix" warning, or just hover-on-class without touching the rest of hover.
- `format` — turn document formatting off per language: `turtle`, `trig`, `sparql`, `n3`,
`jsonld` (all default `true`).
- `prefix_format` — `"turtle"` (default) writes `@prefix ex: <…>.`, `"sparql"` writes
`PREFIX ex: <…>`. Used when inserting prefixes, and the Turtle formatter rewrites existing
declarations to match.

If `initializationOptions` contains an invalid value, SWLS logs a warning to the editor's output
and falls back to the defaults.

## Documentation

Expand Down
19 changes: 15 additions & 4 deletions core/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use goto_type::GotoTypeRequest;
use references::ReferencesRequest;
use request::{GotoTypeDefinitionParams, GotoTypeDefinitionResponse};
use crate::text::LineIndex;
use tracing::{debug, error, info, instrument};
use tracing::{debug, error, info, instrument, warn};

use crate::{
client::Client,
Expand Down Expand Up @@ -143,9 +143,20 @@ impl<C: Client> Backend<C> {
info!("Initialize");

let workspaces = init.workspace_folders.clone().unwrap_or_default();
let config: Config =
serde_json::from_value(init.initialization_options.clone().unwrap_or_default())
.unwrap_or_default();
let config: Config = match init.initialization_options.clone() {
None | Some(serde_json::Value::Null) => Config::default(),
Some(options) => match serde_json::from_value(options) {
Ok(config) => config,
Err(e) => {
// A single wrong value would otherwise reset every setting
// without a trace.
let msg = format!("Invalid swls initializationOptions, using defaults: {e}");
warn!("{msg}");
self.client.log_message(MessageType::WARNING, msg).await;
Config::default()
}
},
};

let mut server_config = ServerConfig { config, workspaces };

Expand Down
105 changes: 86 additions & 19 deletions core/src/components/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,51 +163,74 @@ pub struct Config {
pub jsonld: Option<bool>,
/// Enable sparql
pub sparql: Option<bool>,
/// Per-language `textDocument/formatting` toggles. Formatting is disabled by
/// default for every language except Turtle.
#[serde(default)]
pub format: FormatConfig,
/// Extra local configuration
#[serde(flatten)]
pub local: LocalConfig,
}

/// Per-language toggles for `textDocument/formatting`.
///
/// Each field is an `Option<bool>`, but the *effective* default differs per
/// language: Turtle formatting is on unless explicitly disabled, while every
/// other language is off unless explicitly enabled. Use the accessor methods
/// ([`FormatConfig::turtle`] etc.) to resolve a field to its effective value
/// rather than reading the `Option` directly.
/// Each field is an `Option<bool>`; formatting is on for every language unless
/// explicitly disabled. Use the accessor methods ([`FormatConfig::turtle`]
/// etc.) to resolve a field to its effective value rather than reading the
/// `Option` directly.
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct FormatConfig {
/// Enable Turtle formatting (default: `true`).
#[serde(skip_serializing_if = "Option::is_none")]
pub turtle: Option<bool>,
/// Enable TriG formatting (default: `false`).
/// Enable TriG formatting (default: `true`).
#[serde(skip_serializing_if = "Option::is_none")]
pub trig: Option<bool>,
/// Enable N3 formatting (default: `false`).
/// Enable N3 formatting (default: `true`).
#[serde(skip_serializing_if = "Option::is_none")]
pub n3: Option<bool>,
/// Enable JSON-LD formatting (default: `false`).
/// Enable JSON-LD formatting (default: `true`).
#[serde(skip_serializing_if = "Option::is_none")]
pub jsonld: Option<bool>,
/// Enable SPARQL formatting (default: `true`).
#[serde(skip_serializing_if = "Option::is_none")]
pub sparql: Option<bool>,
}

impl FormatConfig {
/// Combines this config with another, giving precedence to the other's
/// explicitly set toggles.
pub fn combine(&mut self, other: FormatConfig) {
let FormatConfig {
turtle,
trig,
n3,
jsonld,
sparql,
} = other;
self.turtle = turtle.or(self.turtle);
self.trig = trig.or(self.trig);
self.n3 = n3.or(self.n3);
self.jsonld = jsonld.or(self.jsonld);
self.sparql = sparql.or(self.sparql);
}

/// Whether Turtle formatting is enabled (defaults to `true`).
pub fn turtle(&self) -> bool {
self.turtle.unwrap_or(true)
}
/// Whether TriG formatting is enabled (defaults to `false`).
/// Whether TriG formatting is enabled (defaults to `true`).
pub fn trig(&self) -> bool {
self.trig.unwrap_or(false)
self.trig.unwrap_or(true)
}
/// Whether N3 formatting is enabled (defaults to `false`).
/// Whether N3 formatting is enabled (defaults to `true`).
pub fn n3(&self) -> bool {
self.n3.unwrap_or(false)
self.n3.unwrap_or(true)
}
/// Whether JSON-LD formatting is enabled (defaults to `false`).
/// Whether JSON-LD formatting is enabled (defaults to `true`).
pub fn jsonld(&self) -> bool {
self.jsonld.unwrap_or(false)
self.jsonld.unwrap_or(true)
}
/// Whether SPARQL formatting is enabled (defaults to `true`).
pub fn sparql(&self) -> bool {
self.sparql.unwrap_or(true)
}
}

Expand All @@ -226,6 +249,9 @@ pub struct LocalConfig {
pub completion: CompletionConfig,
/// Preferred way to write Turtle/TriG prefix declarations when inserting them.
pub prefix_format: Option<PrefixFormat>,
/// Per-language `textDocument/formatting` toggles. Formatting is enabled
/// by default for every language.
pub format: FormatConfig,
/// Namespaces for which IRIs used as properties (predicates) must be defined in a
/// known ontology. Predicate IRIs that start with one of these namespaces but are
/// not a known property (and not in [`allowed_properties`]) are flagged with a warning.
Expand Down Expand Up @@ -351,6 +377,7 @@ impl LocalConfig {
if other.prefix_format.is_some() {
self.prefix_format = other.prefix_format;
}
self.format.combine(other.format);
self.closed_namespaces.extend(other.closed_namespaces);
self.allowed_properties.extend(other.allowed_properties);
}
Expand Down Expand Up @@ -430,7 +457,6 @@ impl Default for Config {
n3: None,
jsonld: None,
sparql: None,
format: FormatConfig::default(),
local: LocalConfig::default(),
}
}
Expand All @@ -439,3 +465,44 @@ impl Default for Config {
fn debug() -> String {
String::from("debug")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn format_from_config_file() {
let local: LocalConfig =
serde_json::from_str(r#"{ "format": { "sparql": false } }"#).unwrap();
assert!(!local.format.sparql());
assert!(local.format.turtle(), "unset toggles keep their defaults");
assert!(local.format.n3());
}

#[test]
fn format_from_initialization_options() {
let config: Config =
serde_json::from_value(serde_json::json!({ "format": { "trig": false } })).unwrap();
assert!(!config.local.format.trig());
}

#[test]
fn later_sources_override_set_toggles_only() {
let mut config: LocalConfig =
serde_json::from_str(r#"{ "format": { "sparql": true, "turtle": false } }"#).unwrap();
let workspace: LocalConfig =
serde_json::from_str(r#"{ "format": { "sparql": false } }"#).unwrap();
config.combine(workspace);
assert!(!config.format.sparql());
assert!(
!config.format.turtle(),
"toggles the workspace leaves unset are kept"
);
}

#[test]
fn saving_config_writes_no_unset_toggles() {
let json = serde_json::to_value(LocalConfig::default()).unwrap();
assert_eq!(json["format"], serde_json::json!({}));
}
}
2 changes: 1 addition & 1 deletion lang-jsonld/src/ecs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ pub(crate) fn format_jsonld_system(
config: Res<ServerConfig>,
) {
use swls_core::lsp_types::{Position, Range};
if !config.config.format.jsonld() {
if !config.config.local.format.jsonld() {
tracing::debug!("JSON-LD formatting disabled by config");
return;
}
Expand Down
25 changes: 24 additions & 1 deletion lang-n3/src/ecs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,37 @@ mod tests {
messages
);
}

#[test]
fn n3_formatting_uses_n3_rules() {
let (mut world, _rx) = setup_world(TestClient::new(), crate::setup_world::<TestClient>);
{
let mut cfg = world.resource_mut::<ServerConfig>();
cfg.config.n3 = Some(true);
}
let src = "@prefix ex: <http://ex/>. { ?x ex:parent ?y } => { ?x ex:child ?y }. ex:a ex:p ex:b; ex:q ex:c.";
let entity = create_file(&mut world, src, "http://example.com/rules.n3", "n3", Open);
world.run_schedule(ParseLabel);
world.entity_mut(entity).insert(FormatRequest(None));
world.run_schedule(FormatLabel);
let formatted = world
.entity_mut(entity)
.take::<FormatRequest>()
.and_then(|r| r.0)
.map(|edits| edits[0].new_text.clone());
assert_eq!(
formatted.as_deref(),
Some("@prefix ex: <http://ex/>.\n\n{ ?x ex:parent ?y } => { ?x ex:child ?y }.\n\nex:a ex:p ex:b;\n ex:q ex:c.\n")
);
}
}

pub(crate) fn format_n3_system(
mut query: Query<(&RopeC, &Wrapped<GreenNode>, &mut FormatRequest), With<N3Lang>>,
config: Res<ServerConfig>,
) {
use swls_core::lsp_types::{Position, Range};
if !config.config.format.n3() {
if !config.config.local.format.n3() {
tracing::debug!("N3 formatting disabled by config");
return;
}
Expand Down
Loading
Loading