diff --git a/Cargo.lock b/Cargo.lock index 38c1b5f8b2..d3a8370945 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.6" @@ -285,7 +291,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9cf7a3ee41342dd7b5a5d82e200d0e8efb933169247fce853b4ad633d51e87d" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "bevy_ecs_macros", "bevy_platform", "bevy_ptr", @@ -2198,6 +2204,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "pretty" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156" +dependencies = [ + "arrayvec 0.5.2", + "typed-arena", + "unicode-width", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -2455,12 +2472,11 @@ dependencies = [ [[package]] name = "rdf-parsers" version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1eee344235c9e9d033caed31059cf226f24e81e0621297c57506a0b513a5a8" dependencies = [ "async-trait", "logos", "oxiri", + "pretty", "rowan", "similar", ] @@ -2682,7 +2698,7 @@ version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "borsh", "bytes", "num-traits", @@ -4000,6 +4016,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + [[package]] name = "typeid" version = "1.0.3" @@ -4131,7 +4153,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index dff5aa6ea1..c3465229c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 29e0bf4a8b..a6f91d3a09 100644 --- a/README.md +++ b/README.md @@ -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` | @@ -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 diff --git a/core/src/backend.rs b/core/src/backend.rs index 9de623f7fe..2bbe71530c 100644 --- a/core/src/backend.rs +++ b/core/src/backend.rs @@ -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, @@ -143,9 +143,20 @@ impl Backend { 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 }; diff --git a/core/src/components/config.rs b/core/src/components/config.rs index cda74bbe29..e4998aab5c 100644 --- a/core/src/components/config.rs +++ b/core/src/components/config.rs @@ -163,10 +163,6 @@ pub struct Config { pub jsonld: Option, /// Enable sparql pub sparql: Option, - /// 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, @@ -174,40 +170,67 @@ pub struct Config { /// Per-language toggles for `textDocument/formatting`. /// -/// Each field is an `Option`, 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`; 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, - /// Enable TriG formatting (default: `false`). + /// Enable TriG formatting (default: `true`). + #[serde(skip_serializing_if = "Option::is_none")] pub trig: Option, - /// Enable N3 formatting (default: `false`). + /// Enable N3 formatting (default: `true`). + #[serde(skip_serializing_if = "Option::is_none")] pub n3: Option, - /// Enable JSON-LD formatting (default: `false`). + /// Enable JSON-LD formatting (default: `true`). + #[serde(skip_serializing_if = "Option::is_none")] pub jsonld: Option, + /// Enable SPARQL formatting (default: `true`). + #[serde(skip_serializing_if = "Option::is_none")] + pub sparql: Option, } 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) } } @@ -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, + /// 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. @@ -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); } @@ -430,7 +457,6 @@ impl Default for Config { n3: None, jsonld: None, sparql: None, - format: FormatConfig::default(), local: LocalConfig::default(), } } @@ -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!({})); + } +} diff --git a/lang-jsonld/src/ecs/mod.rs b/lang-jsonld/src/ecs/mod.rs index 5852299304..f79685e0c8 100644 --- a/lang-jsonld/src/ecs/mod.rs +++ b/lang-jsonld/src/ecs/mod.rs @@ -540,7 +540,7 @@ pub(crate) fn format_jsonld_system( config: Res, ) { 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; } diff --git a/lang-n3/src/ecs/mod.rs b/lang-n3/src/ecs/mod.rs index c4fdf403cd..cece8e6cc2 100644 --- a/lang-n3/src/ecs/mod.rs +++ b/lang-n3/src/ecs/mod.rs @@ -230,6 +230,29 @@ mod tests { messages ); } + + #[test] + fn n3_formatting_uses_n3_rules() { + let (mut world, _rx) = setup_world(TestClient::new(), crate::setup_world::); + { + let mut cfg = world.resource_mut::(); + cfg.config.n3 = Some(true); + } + let src = "@prefix 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::() + .and_then(|r| r.0) + .map(|edits| edits[0].new_text.clone()); + assert_eq!( + formatted.as_deref(), + Some("@prefix 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( @@ -237,7 +260,7 @@ pub(crate) fn format_n3_system( config: Res, ) { 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; } diff --git a/lang-sparql/src/ecs/mod.rs b/lang-sparql/src/ecs/mod.rs index a318b9549a..0be113d251 100644 --- a/lang-sparql/src/ecs/mod.rs +++ b/lang-sparql/src/ecs/mod.rs @@ -33,12 +33,48 @@ pub fn setup_parse(world: &mut World) { }); } +pub fn setup_format(world: &mut World) { + world.schedule_scope(FormatLabel, |_, schedule| { + schedule.add_systems(format_sparql_system); + }); +} + +fn format_sparql_system( + mut query: Query< + (&RopeC, &Wrapped, &mut FormatRequest), + (With, Without), + >, + config: Res, +) { + use swls_core::lsp_types::{Position, Range, TextEdit}; + if !config.config.local.format.sparql() { + tracing::debug!("SPARQL formatting disabled by config"); + return; + } + for (source, node, mut request) in &mut query { + if request.0.is_some() { + tracing::debug!("Didn't format with the sparql format system, already formatted"); + continue; + } + tracing::debug!("Formatting with sparql format system"); + + let root = rowan::SyntaxNode::new_root(node.0.clone()); + let formatted = rdf_parsers::sparql::format::format(&root, 80); + + request.0 = Some(vec![TextEdit::new( + Range::new( + Position::new(0, 0), + Position::new(source.0.len_lines() as u32 + 1, 0), + ), + formatted, + )]); + } +} + pub fn setup_completion(world: &mut World) { use swls_core::feature::completion::*; world.schedule_scope(Label, |_, schedule| { - schedule.add_systems(( - variable_completion.after(generate_completions), - )); + schedule.add_systems((variable_completion.after(generate_completions),)); }); } @@ -80,9 +116,16 @@ fn parse_sparql_system( parser::{Lang, Rule, SyntaxKind}, }; + // `.ru` files hold SPARQL updates (INSERT DATA, DELETE ... WHERE, ...), + // which are not queries. + let unit = if label.0.path().ends_with(".ru") { + SyntaxKind::UpdateUnit + } else { + SyntaxKind::QueryUnit + }; let prev = prev_infos.get(label.as_str()); let (parse, new_prev) = rdf_parsers::parse_incremental( - Rule::new(SyntaxKind::QueryUnit), + Rule::new(unit), source.0.as_str(), prev, IncrementalBias::default(), @@ -103,6 +146,7 @@ fn parse_sparql_system( commands .entity(entity) .insert((element, Errors(errors), CstTokens(cst_tokens))) + .insert(Wrapped(syntax.green().into_owned())) .remove::(); } else { commands @@ -218,3 +262,87 @@ pub fn variable_completion( } } } + +#[cfg(test)] +mod format_tests { + use swls_core::prelude::*; + use swls_test_utils::{create_file, setup_world, TestClient}; + use test_log::test; + + fn format_with(sparql_enabled: Option) -> Option { + let (mut world, _rx) = setup_world(TestClient::new(), crate::setup_world); + + let mut cfg = ServerConfig::default(); + cfg.config.local.format.sparql = sparql_enabled; + world.insert_resource(cfg); + + let entity = create_file( + &mut world, + "SELECT ?s WHERE { ?s ?o . FILTER(?o>5) }", + "http://example.com/query.rq", + "sparql", + Open, + ); + // ParseLabel populates the green node the formatter consumes. + world.run_schedule(ParseLabel); + + world.entity_mut(entity).insert(FormatRequest(None)); + world.run_schedule(FormatLabel); + + world + .entity_mut(entity) + .take::() + .and_then(|r| r.0) + .map(|edits| edits[0].new_text.clone()) + } + + fn format_update(src: &str) -> (bool, Option) { + let (mut world, _rx) = setup_world(TestClient::new(), crate::setup_world); + let mut cfg = ServerConfig::default(); + cfg.config.local.format.sparql = None; + world.insert_resource(cfg); + + let entity = create_file(&mut world, src, "http://example.com/update.ru", "sparql", Open); + world.run_schedule(ParseLabel); + let dirty = world.entity(entity).contains::(); + + world.entity_mut(entity).insert(FormatRequest(None)); + world.run_schedule(FormatLabel); + let formatted = world + .entity_mut(entity) + .take::() + .and_then(|r| r.0) + .map(|edits| edits[0].new_text.clone()); + (dirty, formatted) + } + + #[test] + fn update_files_parse_and_format() { + let (dirty, formatted) = format_update( + "PREFIX ex: INSERT DATA { ex:a ex:b ex:c } ; DELETE { ?s ex:p ?o } WHERE { ?s ex:p ?o . FILTER(?o>1) }", + ); + assert!(!dirty, "a valid update should parse without errors"); + assert_eq!( + formatted.as_deref(), + Some( + "PREFIX ex: \n\nINSERT DATA {\n ex:a ex:b ex:c\n};\n\nDELETE {\n ?s ex:p ?o\n}\nWHERE {\n ?s ex:p ?o.\n FILTER (?o > 1)\n}\n" + ) + ); + } + + #[test] + fn sparql_formatting_can_be_disabled() { + assert!( + format_with(Some(false)).is_none(), + "SPARQL formatting should not run when disabled via config" + ); + } + + #[test] + fn sparql_formatting_on_by_default() { + assert_eq!( + format_with(None).as_deref(), + Some("SELECT ?s\nWHERE {\n ?s ?o.\n FILTER (?o > 5)\n}\n") + ); + } +} diff --git a/lang-sparql/src/lib.rs b/lang-sparql/src/lib.rs index 6ab9785f61..8fb98e277d 100644 --- a/lang-sparql/src/lib.rs +++ b/lang-sparql/src/lib.rs @@ -7,7 +7,7 @@ use swls_lang_rdf_base::register_rdf_lang; use swls_lang_turtle::lang::parser::TurtleParseError; pub mod ecs; -use crate::ecs::{setup_completion, setup_parse}; +use crate::ecs::{setup_completion, setup_format, setup_parse}; pub mod lang; pub fn setup_world(world: &mut World) { @@ -20,6 +20,7 @@ pub fn setup_world(world: &mut World) { ); setup_parse(world); setup_completion(world); + setup_format(world); } #[derive(Debug, Component, Default)] diff --git a/lang-trig/src/ecs/mod.rs b/lang-trig/src/ecs/mod.rs index 5a101a0434..8a31ad695b 100644 --- a/lang-trig/src/ecs/mod.rs +++ b/lang-trig/src/ecs/mod.rs @@ -24,7 +24,6 @@ pub fn setup_parsing(world: &mut World) { }); } - fn extract_trig_cst_tokens( node: &rowan::SyntaxNode, ) -> Vec> { @@ -147,7 +146,7 @@ pub(crate) fn format_trig_system( config: Res, ) { use swls_core::lsp_types::{Position, Range}; - if !config.config.format.trig() { + if !config.config.local.format.trig() { tracing::debug!("TriG formatting disabled by config"); return; } @@ -182,7 +181,7 @@ mod tests { let (mut world, _rx) = setup_world(TestClient::new(), crate::setup_world::); let mut cfg = ServerConfig::default(); - cfg.config.format.trig = trig_enabled; + cfg.config.local.format.trig = trig_enabled; world.insert_resource(cfg); let entity = create_file( @@ -206,20 +205,15 @@ mod tests { } #[test] - fn trig_formatting_off_by_default() { - assert!( - format_with(None).is_none(), - "TriG formatting should be disabled unless explicitly enabled" - ); + fn trig_formatting_on_by_default() { + assert_eq!(format_with(None).as_deref(), Some(" ;\n .\n")); } #[test] - fn trig_formatting_can_be_enabled() { - let formatted = format_with(Some(true)); + fn trig_formatting_can_be_disabled() { assert!( - formatted.is_some(), - "TriG formatting should run when enabled via config" + format_with(Some(false)).is_none(), + "TriG formatting should not run when disabled via config" ); - assert!(formatted.unwrap().contains(" ;")); } } diff --git a/lang-turtle/src/ecs/format.rs b/lang-turtle/src/ecs/format.rs index 283784b043..06d514ebca 100644 --- a/lang-turtle/src/ecs/format.rs +++ b/lang-turtle/src/ecs/format.rs @@ -13,19 +13,19 @@ use crate::{ pub fn format_turtle_system( mut query: Query< - (&RopeC, &Element, &Comments, &mut FormatRequest), + (&RopeC, &Wrapped, &mut FormatRequest), (With, Without), >, config: Res, ) { debug!("Format turtle system"); - if !config.config.format.turtle() { + if !config.config.local.format.turtle() { debug!("Turtle formatting disabled by config"); return; } - for (source, turtle, comments, mut request) in &mut query { + for (source, node, mut request) in &mut query { if request.0.is_some() { debug!("Didn't format with the turtle format system, already formatted"); continue; @@ -43,16 +43,8 @@ pub fn format_turtle_system( }) .unwrap_or_default(); - let formatted = format_turtle( - &turtle.0, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments.0, - &source.0, - style, - ); + let root = rowan::SyntaxNode::new_root(node.0.clone()); + let formatted = Some(format_turtle(&root, style)); request.0 = formatted.map(|x| { vec![swls_core::lsp_types::TextEdit::new( @@ -92,6 +84,6 @@ mod test { assert!(m_formatted.is_some()); let formatted = &m_formatted.unwrap()[0].new_text; - assert_eq!(formatted, "@prefix foaf: <>.\n\n"); + assert_eq!(formatted, "@prefix foaf: <>.\n"); } } diff --git a/lang-turtle/src/lang/formatter.rs b/lang-turtle/src/lang/formatter.rs index 5fb99f23a0..b99c29a5f1 100644 --- a/lang-turtle/src/lang/formatter.rs +++ b/lang-turtle/src/lang/formatter.rs @@ -1,705 +1,112 @@ -use std::{ - io::{self, Cursor, Write}, - ops::Range, +//! Turtle formatting. +//! +//! Layout comes from the generated formatter in `rdf_parsers` (grammar hints +//! plus `rdf_parsers::turtle::format::TurtleHook`). The only rule added here +//! is rewriting prefix and base declarations to the configured style. + +use rdf_parsers::{ + format::{Doc, FormatHook, Formatter}, + turtle::{ + format::TurtleHook, + parser::{format::format_with, Lang, SyntaxKind, SyntaxNode}, + }, }; -use swls_core::text::LineIndex; -use swls_core::{ - lsp_types::FormattingOptions, - prelude::{spanned, Spanned}, -}; -use tracing::{trace, warn}; - -use swls_lang_rdf_base::traits::{Base, BlankNode, Term, Triple, Turtle, TurtlePrefix, PO}; - -type Buf = Cursor>; #[derive(Default, Clone, Copy)] pub enum PrefixStyle { #[default] OldSchool, Sparql, } -struct FormatState<'a> { - indent_level: usize, - indent: String, - buf: Buf, - line_start: u64, - comments: &'a [Spanned], - comments_idx: usize, - tail: Spanned, - line_count: usize, - prefix_style: PrefixStyle, -} - -impl<'a> FormatState<'a> { - fn new( - options: FormattingOptions, - buf: Buf, - comments: &'a [Spanned], - source: &'a LineIndex, - prefix_style: PrefixStyle, - ) -> Self { - let mut indent = String::new(); - for _ in 0..options.tab_size { - indent.push(' '); - } - - // Sentinel span past the end of the document, so the trailing comment - // bucket sorts after every real span. - let tail = spanned( - String::new(), - source.len_bytes() + 1..source.len_bytes() + 1, - ); - - Self { - tail, - line_start: 0, - indent_level: 0, - indent, - buf, - comments, - comments_idx: 0, - line_count: 0, - prefix_style, - } - } - fn check_comments(&mut self, span: &Range) -> io::Result { - trace!("checking comments with span {:?}", span); - let mut first = true; - loop { - let current = self.comments.get(self.comments_idx).unwrap_or(&self.tail); - - if current.1.start > span.start { - break; - } - - first = false; - write!(self.buf, "{}", current.0)?; - self.new_line()?; - self.comments_idx += 1; - } - Ok(!first) - } - fn current_line_length(&self) -> u64 { - self.buf.position() - self.line_start - } - fn new_line(&mut self) -> io::Result<()> { - self.line_count += 1; - write!(self.buf, "\n")?; - self.line_start = self.buf.position(); - for _ in 0..self.indent_level { - write!(self.buf, "{}", &self.indent)?; - } - Ok(()) - } - fn inc(&mut self) { - self.indent_level += 1; - } - fn decr(&mut self) { - self.indent_level -= 1; - } +/// Format a Turtle document from its syntax tree. +pub fn format_turtle(root: &SyntaxNode, prefix_style: impl Into) -> String { + let hook = PrefixStyleHook { + style: prefix_style.into(), + }; + format_with(root, 80, &hook) } -impl FormatState<'_> { - fn write_turtle(&mut self, turtle: &Turtle) -> io::Result<()> { - if let Some(ref b) = turtle.base { - self.check_comments(&b.1)?; - self.write_base(b)?; - self.new_line()?; - } - for p in &turtle.prefixes { - self.check_comments(&p.1)?; - self.write_prefix(p)?; - self.new_line()?; - } - - let mut prev_line = 0; - - for t in &turtle.triples { - if prev_line + 1 < self.line_count { - self.new_line()?; - } - prev_line = self.line_count; - self.check_comments(&t.1)?; - self.write_triple(&t)?; - self.new_line()?; - // request_newline = t.0.po.len() > 1 || t.0.po[0].0.object.len() > 1; - } - self.new_line()?; - - for i in self.comments_idx..self.comments.len() { - write!(self.buf, "{}", self.comments[i].0)?; - self.new_line()?; - } +/// [`TurtleHook`] that also writes prefix and base declarations in one style. +struct PrefixStyleHook { + style: PrefixStyle, +} - Ok(()) +impl FormatHook for PrefixStyleHook { + fn keep(&self, element: &rowan::SyntaxElement) -> bool { + TurtleHook.keep(element) } - fn write_prefix(&mut self, prefix: &TurtlePrefix) -> io::Result<()> { - match self.prefix_style { - PrefixStyle::OldSchool => { - write!(self.buf, "@prefix {}: {}.", prefix.prefix.0, prefix.value.0) - } - PrefixStyle::Sparql => { - write!(self.buf, "PREFIX {}: {}", prefix.prefix.0, prefix.value.0) - } - } + fn node(&self, node: &SyntaxNode, fmt: &Formatter<'_, Lang>) -> Option { + self.directive(node).or_else(|| TurtleHook.node(node, fmt)) } +} - fn write_base(&mut self, base: &Base) -> io::Result<()> { - match self.prefix_style { - PrefixStyle::OldSchool => { - write!(self.buf, "@base {}.", base.1 .0) - } - PrefixStyle::Sparql => { - write!(self.buf, "BASE {}", base.1 .0) - } +impl PrefixStyleHook { + fn directive(&self, node: &SyntaxNode) -> Option { + let text = |kind| { + node.descendants_with_tokens() + .find(|e| e.kind() == kind && e.as_token().is_some()) + .map(|e| e.as_token().unwrap().text().to_string()) + }; + // Leave declarations with comments inside as they are. + if text(SyntaxKind::Comment).is_some() { + return None; } - } - - fn write_bnode(&mut self, bnode: &BlankNode) -> io::Result<()> { - match bnode { - BlankNode::Named(x, _) => write!(self.buf, "_:{}", x)?, - BlankNode::Unnamed(pos, _, _) => { - if pos.len() == 0 { - return write!(self.buf, "[ ]"); - } - if pos.len() == 1 { - write!(self.buf, "[ ")?; - self.write_po(&pos[0])?; - return write!(self.buf, " ]"); + let iri = text(SyntaxKind::Iriref)?; + let out = match (node.kind(), self.style) { + (SyntaxKind::PrefixId | SyntaxKind::SparqlPrefix, style) => { + let prefix = text(SyntaxKind::PnameNs)?; + match style { + PrefixStyle::OldSchool => format!("@prefix {prefix} {iri}."), + PrefixStyle::Sparql => format!("PREFIX {prefix} {iri}"), } - let is_first_of_line = self.current_line_length() == 0; - self.inc(); - write!(self.buf, "[")?; - let should_skip = if is_first_of_line { - write!(self.buf, " ")?; - self.write_po(&pos[0])?; - write!(self.buf, ";")?; - 1 - } else { - 0 - }; - for po in pos.iter().skip(should_skip) { - self.new_line()?; - self.check_comments(&po.1)?; - self.write_po(&po)?; - write!(self.buf, ";")?; - } - self.decr(); - self.new_line()?; - write!(self.buf, "]")?; } - BlankNode::Invalid => return Err(io::Error::new(io::ErrorKind::Other, "")), - } - Ok(()) - } - - fn write_collection(&mut self, coll: &Vec>) -> io::Result<()> { - if coll.is_empty() { - return write!(self.buf, "( )"); - } - - let mut should_indent = false; - let start = self.buf.position(); - let current_line = self.line_count; - - write!(self.buf, "( ")?; - - self.check_comments(&coll[0].1)?; - self.write_term(&coll[0])?; - - for po in coll.iter().skip(1) { - self.check_comments(&po.1)?; - write!(self.buf, " ")?; - self.write_term(&po)?; - if self.current_line_length() > 80 { - should_indent = true; - break; + (SyntaxKind::Base | SyntaxKind::SparqlBase, PrefixStyle::OldSchool) => { + format!("@base {iri}.") } - } - write!(self.buf, " )")?; - - if should_indent { - self.buf.set_position(start); - self.line_count = current_line; - write!(self.buf, "(")?; - self.inc(); - for po in coll.iter() { - self.new_line()?; - self.check_comments(&po.1)?; - self.write_term(&po)?; + (SyntaxKind::Base | SyntaxKind::SparqlBase, PrefixStyle::Sparql) => { + format!("BASE {iri}") } - self.decr(); - self.new_line()?; - write!(self.buf, ")")?; - } - - Ok(()) + _ => return None, + }; + Some(Doc::text(out)) } - - fn write_term(&mut self, term: &Term) -> io::Result<()> { - match term { - Term::Literal(s) => write!(self.buf, "{}", s)?, - Term::BlankNode(b) => self.write_bnode(b)?, - Term::NamedNode(n) => write!(self.buf, "{}", n)?, - Term::Collection(ts) => self.write_collection(ts)?, - Term::Invalid => { - return Err(io::Error::new( - io::ErrorKind::Other, - "cannot format turtle with invalid terms", - )) - } - Term::Variable(_) => { - return Err(io::Error::new( - io::ErrorKind::Other, - "cannot format turtle with variables", - )) - } - } - Ok(()) - } - - fn write_po(&mut self, po: &PO) -> io::Result<()> { - write!(self.buf, "{} ", po.predicate.0)?; - self.write_term(&po.object[0])?; - let mut should_indent = false; - - let start = self.buf.position(); - let current_line = self.line_count; - for i in 1..po.object.len() { - write!(self.buf, ", ")?; - self.write_term(&po.object[i])?; - - if self.current_line_length() > 80 { - should_indent = true; - break; - } - } - - if should_indent { - self.buf.set_position(start); - self.line_count = current_line; - self.inc(); - for i in 1..po.object.len() { - write!(self.buf, ",")?; - self.new_line()?; - self.check_comments(&po.object[i].1)?; - self.write_term(&po.object[i])?; - } - self.decr(); - } - - Ok(()) - } - - fn write_triple(&mut self, triple: &Triple) -> io::Result<()> { - match &triple.subject.0 { - Term::BlankNode(bn) => self.write_bnode(bn)?, - Term::NamedNode(n) => write!(self.buf, "{}", n)?, - _ => write!(self.buf, "invalid")?, - } - if triple.po.is_empty() { - write!(self.buf, ".")?; - return Ok(()); - } - write!(self.buf, " ")?; - self.write_po(&triple.po[0])?; - if triple.po.len() == 1 { - write!(self.buf, ".")?; - return Ok(()); - } - write!(self.buf, ";")?; - self.inc(); - - self.new_line()?; - self.check_comments(&triple.po[1].1)?; - self.write_po(&triple.po[1])?; - - if triple.po.len() == 2 { - self.decr(); - write!(self.buf, ".")?; - return Ok(()); - } - - for i in 2..triple.po.len() { - write!(self.buf, ";")?; - self.new_line()?; - self.check_comments(&triple.po[i].1)?; - self.write_po(&triple.po[i])?; - } - - write!(self.buf, ".")?; - self.decr(); - Ok(()) - } -} - -pub fn format_turtle( - turtle: &Turtle, - config: FormattingOptions, - comments: &[Spanned], - source: &LineIndex, - prefix_style: impl Into, -) -> Option { - let buf: Buf = Cursor::new(Vec::new()); - let mut state = FormatState::new(config, buf, comments, source, prefix_style.into()); - match state.write_turtle(turtle) { - Ok(_) => tracing::debug!("Format successful"), - Err(e) => { - warn!("Format unsuccessful {:?}", e); - return None; - } - } - String::from_utf8(state.buf.into_inner()).ok() } #[cfg(test)] mod tests { + use super::{format_turtle, PrefixStyle}; + use crate::lang::parser::parse_new; - use std::str::FromStr; - - use rdf_parsers::turtle::SyntaxKind; - use rowan::NodeOrToken; - use swls_core::prelude::Spanned; - use swls_core::text::LineIndex; - - use crate::lang::{ - formatter::{format_turtle, PrefixStyle}, - parser::parse_new, - Turtle, - }; - - fn parse_turtle(inp: &str, url: &swls_core::lsp_types::Url) -> (Turtle, Vec>) { - let (turtle, errs, _, syntax, _) = parse_new(inp, url.as_str(), None); - for e in &errs { - println!("Parse error: {:?}", e); - } - - // Extract comments from the CST - let mut comments: Vec> = Vec::new(); - for node_or_token in syntax.descendants_with_tokens() { - if let NodeOrToken::Token(t) = node_or_token { - if t.kind() == SyntaxKind::Comment { - let range = t.text_range(); - let span = usize::from(range.start())..usize::from(range.end()); - let text = t.text().to_string(); - comments.push(swls_core::prelude::spanned(text, span)); - } - } - } - comments.sort_by_key(|x| x.1.start); - - (turtle, comments) - } - - #[test] - fn easy_format() { - let txt = r#" -@prefix rdf: . -@prefix rdf: . -@base . - -[] a foaf:Name; - foaf:knows ;. -"#; - - let expected = r#"@base . -@prefix rdf: . -@prefix rdf: . - -[ ] a foaf:Name; - foaf:knows . - -"#; - - let url = swls_core::lsp_types::Url::from_str("http://example.com/ns#").unwrap(); - let (output, comments) = parse_turtle(txt, &url); - let formatted = format_turtle( - &output, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments, - &LineIndex::new(txt), - PrefixStyle::OldSchool, - ) - .expect("formatting"); - assert_eq!(formatted, expected); + fn format(input: &str, style: PrefixStyle) -> String { + let (_, _, _, syntax, _) = parse_new(input, "http://example.com/ns#", None); + format_turtle(&syntax, style) } #[test] - fn harder_format_pos() { - let txt = r#" -[] a foaf:Name; - foaf:knows ; foaf:knows2 . - -"#; - - let expected = r#"[ ] a foaf:Name; - foaf:knows ; - foaf:knows2 . - -"#; - - let url = swls_core::lsp_types::Url::from_str("http://example.com/ns#").unwrap(); - let (output, comments) = parse_turtle(txt, &url); - let formatted = format_turtle( - &output, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments, - &LineIndex::new(txt), - PrefixStyle::OldSchool, - ) - .expect("formatting"); - assert_eq!(formatted, expected); + fn writes_prefixes_in_turtle_style() { + let input = + "PREFIX foaf: \nBASE \n foaf:knows ."; + let expected = + "@prefix foaf: .\n@base .\n\n foaf:knows .\n"; + assert_eq!(format(input, PrefixStyle::OldSchool), expected); } #[test] - fn format_blanknodes() { - let txt = r#" - [ foaf:Person; foaf:knows ; foaf:knows ] foaf:knows [ - a foaf:Person; - foaf:knows ; - foaf:knows ; - ] . - -"#; - - let expected = r#"[ foaf:Person; - foaf:knows ; - foaf:knows ; -] foaf:knows [ - a foaf:Person; - foaf:knows ; - foaf:knows ; -]. - -"#; - - let url = swls_core::lsp_types::Url::from_str("http://example.com/ns#").unwrap(); - let (output, comments) = parse_turtle(txt, &url); - let formatted = format_turtle( - &output, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments, - &LineIndex::new(txt), - PrefixStyle::OldSchool, - ) - .expect("formatting"); - assert_eq!(formatted, expected); + fn writes_prefixes_in_sparql_style() { + let input = + "@prefix foaf: .\n@base .\n foaf:knows ."; + let expected = + "PREFIX foaf: \nBASE \n\n foaf:knows .\n"; + assert_eq!(format(input, PrefixStyle::Sparql), expected); } #[test] - fn long_objectlist() { - let txt = r#" - a , , , , . -"#; - - let expected = r#" a , - , - , - , - . - -"#; - - let url = swls_core::lsp_types::Url::from_str("http://example.com/ns#").unwrap(); - let (output, comments) = parse_turtle(txt, &url); - let formatted = format_turtle( - &output, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments, - &LineIndex::new(txt), - PrefixStyle::OldSchool, - ) - .expect("formatting"); - assert_eq!(formatted, expected); - } - - #[test] - fn short_collection() { - let txt = r#" - a (), ( ). -"#; - - let expected = r#" a ( ), ( ). - -"#; - - let url = swls_core::lsp_types::Url::from_str("http://example.com/ns#").unwrap(); - let (output, comments) = parse_turtle(txt, &url); - let formatted = format_turtle( - &output, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments, - &LineIndex::new(txt), - PrefixStyle::OldSchool, - ) - .expect("formatting"); - assert_eq!(formatted, expected); - } - - #[test] - fn long_collection() { - let txt = r#" - a (), ( ). -"#; - - let expected = r#" a ( ), ( - - - - -). - -"#; - - let url = swls_core::lsp_types::Url::from_str("http://example.com/ns#").unwrap(); - let (output, comments) = parse_turtle(txt, &url); - let formatted = format_turtle( - &output, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments, - &LineIndex::new(txt), - PrefixStyle::OldSchool, - ) - .expect("formatting"); - assert_eq!(formatted, expected); - } - - #[test] - fn easy_comments() { - let txt = r#" -# Test this is a cool test or something! - # Another comment! - -[] a foaf:Name; - foaf:knows ; foaf:knows2 . - -"#; - - let expected = r#"# Test this is a cool test or something! -# Another comment! -[ ] a foaf:Name; - foaf:knows ; - foaf:knows2 . - -"#; - - let url = swls_core::lsp_types::Url::from_str("http://example.com/ns#").unwrap(); - let (output, comments) = parse_turtle(txt, &url); - println!("OUtput {:?}", output); - let formatted = format_turtle( - &output, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments, - &LineIndex::new(txt), - PrefixStyle::OldSchool, - ) - .expect("formatting"); - assert_eq!(formatted, expected); - } - - #[test] - fn hard_comments() { - let txt = r#" - -[] a foaf:Name; # Nested comment - foaf:knows ; # Another comment! - foaf:knows2 . - - #trailing comments -"#; - - let expected = r#"[ ] a foaf:Name; - # Nested comment - foaf:knows ; - # Another comment! - foaf:knows2 . - -#trailing comments -"#; - let url = swls_core::lsp_types::Url::from_str("http://example.com/ns#").unwrap(); - let (output, comments) = parse_turtle(txt, &url); - let formatted = format_turtle( - &output, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments, - &LineIndex::new(txt), - PrefixStyle::OldSchool, - ) - .expect("formatting"); - assert_eq!(formatted, expected); - } - - #[test] - fn bug_1() { - let txt = r#" -[] a sh:NodeShape; - sh:targetClass js:Echo; - sh:property [ - sh:class :ReaderChannel; - sh:path js:input; - sh:name "Input Channel" - ], [ - sh:class :WriterChannel; - sh:path js:output; - sh:name "Output Channel" - ]. - -"#; - - let expected = r#"[ ] a sh:NodeShape; - sh:targetClass js:Echo; - sh:property [ - sh:class :ReaderChannel; - sh:path js:input; - sh:name "Input Channel"; - ], [ - sh:class :WriterChannel; - sh:path js:output; - sh:name "Output Channel"; - ]. - -"#; - - let url = swls_core::lsp_types::Url::from_str("http://example.com/ns#").unwrap(); - let (output, comments) = parse_turtle(txt, &url); - let formatted = format_turtle( - &output, - swls_core::lsp_types::FormattingOptions { - tab_size: 2, - ..Default::default() - }, - &comments, - &LineIndex::new(txt), - PrefixStyle::OldSchool, - ) - .expect("formatting"); - assert_eq!(formatted, expected); + fn keeps_layout_rules() { + // Layout itself is tested in rdf-parsers; this checks the hook chain. + let input = "[] a ;

[ ; ], ;. # done"; + let expected = "[ ] a ;\n

[\n ;\n ;\n ], . # done\n"; + assert_eq!(format(input, PrefixStyle::OldSchool), expected); } }