diff --git a/Cargo.lock b/Cargo.lock index 65c7c8e..8724afa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,16 @@ dependencies = [ "object", ] +[[package]] +name = "ariadne" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31beedec3ce83ae6da3a79592b3d8d7afd146a5b15bb9bb940279aced60faa89" +dependencies = [ + "unicode-width", + "yansi", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -814,6 +824,12 @@ dependencies = [ "secp256k1-sys", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -912,10 +928,11 @@ dependencies = [ [[package]] name = "simplicityhl" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "361316795ec753230c421d964ab60940d20d5252c2d39272452528832c9e0ec5" +checksum = "cc05eac7d1f37bfbc182566b4dc2f377be783e2f53a193d833fa27392f004898" dependencies = [ + "ariadne", "base64", "chumsky", "clap", @@ -923,6 +940,7 @@ dependencies = [ "getrandom 0.2.17", "itertools", "miniscript", + "semver", "serde", "serde_json", "simplicity-lang", @@ -930,7 +948,7 @@ dependencies = [ [[package]] name = "simplicityhl-lsp" -version = "0.6.0" +version = "0.7.0" dependencies = [ "env_logger", "miniscript", @@ -1189,6 +1207,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1267,6 +1291,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.8.48" diff --git a/Cargo.toml b/Cargo.toml index 3f3fa35..1f0937c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simplicityhl-lsp" -version = "0.6.0" +version = "0.7.0" edition = "2021" rust-version = "1.85.0" description = "Language Server Protocol (LSP) server for SimplicityHL." @@ -30,7 +30,7 @@ thiserror = "2.0.17" ropey = "1.6.1" miniscript = "12" -simplicityhl = {version = "0.6.0", features = ["docs"]} +simplicityhl = { version = "0.7.0", features = ["docs"] } nom = "8.0.0" [dev-dependencies] diff --git a/README.md b/README.md index ef85248..20a449b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ defaults below are what the server uses when a client sends nothing. { "simplicityhl": { "experimentalFeatures": { - "imports": false + "imports": false, + "enums": false }, "project": { "simplex": { @@ -47,6 +48,8 @@ defaults below are what the server uses when a client sends nothing. - `experimentalFeatures.imports` enables the compiler's unstable `use` / `mod` / `pub` syntax. It is off by default because the feature is unstable in the compiler itself. +- `experimentalFeatures.enums` enables enum declarations and enum match patterns, + likewise unstable in the compiler. - `project.simplex.enabled` looks for the nearest `Simplex.toml` (or `simplex.toml`) in the file's ancestors and honours its `build.src_dir` and `[dependencies]`, resolving path dependencies recursively and locating installed git dependencies under `deps/`. @@ -103,7 +106,7 @@ vim.lsp.config["simplicityhl-lsp"] = { filetypes = { "simf" }, settings = { simplicityhl = { - experimentalFeatures = { imports = true }, + experimentalFeatures = { imports = true, enums = false }, project = { simplex = { enabled = true, manifestPath = "" } }, }, }, diff --git a/src/backend.rs b/src/backend.rs index b373ff3..20c43ce 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -12,14 +12,15 @@ use tokio::sync::RwLock; use tower_lsp_server::jsonrpc::Result; use tower_lsp_server::lsp_types::{ - CompletionOptions, CompletionParams, CompletionResponse, Diagnostic, + CompletionOptions, CompletionParams, CompletionResponse, Diagnostic, DiagnosticSeverity, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, - DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, - DidSaveTextDocumentParams, DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, - ExecuteCommandParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, - HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams, Location, - MarkupContent, MarkupKind, MessageType, OneOf, Range, ReferenceParams, SaveOptions, - SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, + DidChangeWatchedFilesRegistrationOptions, DidChangeWorkspaceFoldersParams, + DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, + DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, ExecuteCommandParams, + FileSystemWatcher, GlobPattern, GotoDefinitionParams, GotoDefinitionResponse, Hover, + HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams, + Location, MarkupContent, MarkupKind, MessageType, OneOf, Range, ReferenceParams, Registration, + SaveOptions, SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams, SemanticTokensResult, SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelp, SignatureHelpOptions, SignatureHelpParams, SymbolKind, TextDocumentSyncCapability, @@ -29,7 +30,11 @@ use tower_lsp_server::lsp_types::{ use tower_lsp_server::{Client, LanguageServer, UriExt}; use miniscript::iter::TreeLike; -use simplicityhl::{error::RichError, parse}; +use simplicityhl::error::{ + Diagnostic as CompilerDiagnostic, DiagnosticManager, Error as CompilerError, + Location as CompilerLocation, Severity as CompilerSeverity, Span, +}; +use simplicityhl::parse; use crate::completion::{self, CompletionProvider}; use crate::config::Settings; @@ -122,6 +127,9 @@ struct ServerConfig { /// Workspace folders, used to resolve relative paths in [`Settings`]. workspace_roots: Vec, + + /// Whether the client supports server-requested file watchers. + watched_files_registration: bool, } #[derive(Debug)] @@ -144,6 +152,13 @@ struct TextDocumentItem<'a> { impl LanguageServer for Backend { async fn initialize(&self, params: InitializeParams) -> Result { let workspace_roots = workspace_roots(¶ms); + let watched_files_registration = params + .capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.did_change_watched_files.as_ref()) + .and_then(|capability| capability.dynamic_registration) + .unwrap_or(false); let settings = params .initialization_options .and_then(|value| Settings::from_json(value).ok()) @@ -151,6 +166,7 @@ impl LanguageServer for Backend { { let mut config = self.config.write().await; config.workspace_roots = workspace_roots; + config.watched_files_registration = watched_files_registration; config.settings = settings; } @@ -205,7 +221,35 @@ impl LanguageServer for Backend { }) } - async fn initialized(&self, _: InitializedParams) {} + async fn initialized(&self, _: InitializedParams) { + if !self.config.read().await.watched_files_registration { + return; + } + + let watchers = ["**/*.simf", "**/Simplex.toml", "**/simplex.toml"] + .into_iter() + .map(|glob| FileSystemWatcher { + glob_pattern: GlobPattern::String(glob.to_string()), + kind: None, + }) + .collect(); + let registration = Registration { + id: "simplicityhl-lsp-watched-files".to_string(), + method: "workspace/didChangeWatchedFiles".to_string(), + register_options: serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { + watchers, + }) + .ok(), + }; + if let Err(error) = self.client.register_capability(vec![registration]).await { + self.client + .log_message( + MessageType::WARNING, + format!("Unable to register file watchers: {error}"), + ) + .await; + } + } async fn shutdown(&self) -> Result<()> { Ok(()) @@ -336,7 +380,7 @@ impl LanguageServer for Backend { let mut raw_tokens: Vec<(u32, u32, u32, u32, u32)> = Vec::new(); // (line, col, len, type, modifiers) for func in &functions { - if func.file_id() != 0 { + if func.span().file_id != 0 { continue; } @@ -467,7 +511,7 @@ impl LanguageServer for Backend { let symbols: Vec = functions .iter() .filter_map(|func| { - if func.file_id() != 0 { + if func.span().file_id != 0 { return None; } @@ -719,7 +763,7 @@ impl LanguageServer for Backend { return Ok(None); }; - let Some(source_file) = doc.linearization_map.get(function.file_id()) else { + let Some(source_file) = doc.linearization_map.get(function.span().file_id) else { return Ok(None); }; @@ -861,10 +905,6 @@ impl Backend { let diagnostics = err .iter() .filter_map(|err| { - let Ok((start, end)) = span_to_positions(err.span(), &rope) else { - return None; - }; - // HACK: We ignoring MainRequired error because right now we cannot parse file as a // library match err.error() { @@ -878,10 +918,33 @@ impl Backend { _ => {} } - Some(Diagnostic::new_simple( - Range::new(start, end), - err.error().to_string(), - )) + // This merged backend owns one open document at a time. Compiler 0.7 + // diagnostics can point into imported files; + // TODO: until multi-document publication is implemented, keep those visible on the + // root document without pretending their byte offsets belong to the root source. + let range = match err.location() { + CompilerLocation::Code(span) if span.file_id == 0 => { + let Ok((start, end)) = span_to_positions(span, &rope) else { + return None; + }; + Range::new(start, end) + } + CompilerLocation::Code(_) + | CompilerLocation::File(_) + | CompilerLocation::Global => Range::default(), + }; + let severity = match err.severity() { + CompilerSeverity::Error => DiagnosticSeverity::ERROR, + CompilerSeverity::Warning => DiagnosticSeverity::WARNING, + }; + + Some(Diagnostic { + range, + severity: Some(severity), + source: Some("simplicityhl".to_string()), + message: err.error().to_string(), + ..Diagnostic::default() + }) }) .collect(); @@ -932,24 +995,25 @@ fn create_document(program: &simplicityhl::parse::Program, text: &str) -> Docume document } -/// Parse and analyze program using [`simplicityhl`] compiler and return an list of [`RichError`] -/// to use in diagnostics. Also creates a [`Document`] if parsing is successful. +/// Parse and analyze a program using the [`simplicityhl`] compiler. +/// Also create a [`Document`] when parsing succeeds. fn parse_program( text: &str, path: &Path, settings: &Settings, workspace_roots: &[PathBuf], -) -> (Vec, Option) { +) -> (Vec, Option) { let unstable_features = settings.unstable_features(); - let mut error_collector = simplicityhl::error::ErrorCollector::new(); + let mut diagnostics = DiagnosticManager::new(); let text: Arc = Arc::from(text); let source_file = simplicityhl::source::SourceFile::new(path, Arc::clone(&text)); let Some(program) = parse::Program::parse_from_str_with_errors( - source_file.clone(), + 0, + text.as_ref(), &unstable_features, - &mut error_collector, + &mut diagnostics, ) else { - return (error_collector.get().to_vec(), None); + return (diagnostics.diagnostics().to_vec(), None); }; let mut document = create_document(&program, text.as_ref()); @@ -961,23 +1025,30 @@ fn parse_program( { Ok(dependencies) => dependencies, Err(err) => { - error_collector.push(RichError::parsing_error(&err.to_string())); + diagnostics.push(CompilerDiagnostic::new( + CompilerError::CannotParse { + msg: err.to_string(), + }, + Span::new(0, 0..0), + )); - return (error_collector.get().to_vec(), Some(document)); + return (diagnostics.diagnostics().to_vec(), Some(document)); } }; - if let Ok(template_program) = TemplateProgram::new_with_dep( + let compiler_diagnostics = match TemplateProgram::new_with_dep( source_file.try_into().expect("name was defined above"), &dependencies, &unstable_features, Box::new(ElementsJetHinter::new()), - ) - .map_err(|e| error_collector = e) - { - document.populate_visible_functions(&template_program); - } + ) { + Ok(template_program) => { + document.populate_visible_functions(&template_program); + template_program.diagnostics().diagnostics().to_vec() + } + Err(diagnostics) => diagnostics.diagnostics().to_vec(), + }; - (error_collector.get().to_vec(), Some(document)) + (compiler_diagnostics, Some(document)) } /// Validate a witness (.wit) file and return diagnostics. @@ -1090,6 +1161,37 @@ mod tests { assert_eq!(doc.functions.map.len(), 2); } + #[test] + fn parse_program_respects_the_enum_feature_setting() { + let source = "enum Choice { Yes, No, }\nfn main() {}\n"; + let (temp, path) = in_temp_project(source); + + let (disabled, _) = parse_program( + source, + &path, + &Settings::default(), + &[temp.path().to_path_buf()], + ); + assert!(disabled.iter().any(|diagnostic| { + matches!( + diagnostic.error(), + Error::UnstableFeature { + feature: simplicityhl::UnstableFeature::Enums + } + ) + })); + + let settings = Settings::from_json(serde_json::json!({ + "experimentalFeatures": { "imports": false, "enums": true } + })) + .expect("valid settings"); + let (enabled, document) = + parse_program(source, &path, &settings, &[temp.path().to_path_buf()]); + + assert!(enabled.is_empty(), "enum should be enabled: {enabled:?}"); + assert!(document.is_some()); + } + #[test] #[ignore = "TODO we need to also create a file with a path so that could work"] fn test_parse_program_invalid_ast() { diff --git a/src/config.rs b/src/config.rs index 6a88854..7a60448 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,8 +20,8 @@ pub struct Settings { pub struct ExperimentalFeatures { /// Enables `use`, `mod`, `pub`, aliases, and multi-file dependency resolution. pub imports: bool, - // TODO: `enums` is absent: `UnstableFeature::Enums` does not exist in the - // released compiler, so the setting would silently do nothing. + /// Enables enum declarations and enum match patterns. + pub enums: bool, } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)] @@ -98,6 +98,9 @@ impl Settings { if self.experimental_features.imports { enabled.push(UnstableFeature::Imports); } + if self.experimental_features.enums { + enabled.push(UnstableFeature::Enums); + } UnstableFeatures::new(enabled) } } @@ -111,6 +114,7 @@ mod tests { let settings = Settings::default(); assert!(!settings.experimental_features.imports); + assert!(!settings.experimental_features.enums); assert!(settings.project.simplex.enabled); } @@ -132,6 +136,7 @@ mod tests { .expect("valid settings"); assert!(settings.experimental_features.imports); + assert!(!settings.experimental_features.enums); assert_eq!( settings.project.simplex.manifest_path, "config/Simplex.toml" @@ -145,12 +150,12 @@ mod tests { #[test] fn accepts_an_unwrapped_configuration_section() { - // `enums` is not a field yet; an editor that still sends it must not break parsing. let settings = Settings::from_json(serde_json::json!({ "experimentalFeatures": { "imports": true, "enums": true } })) .expect("valid settings"); assert!(settings.experimental_features.imports); + assert!(settings.experimental_features.enums); } } diff --git a/src/utils.rs b/src/utils.rs index 27dbc9f..5c0df24 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,7 +12,7 @@ use crate::completion; use crate::error::LspError; pub fn span_contains(a: &simplicityhl::error::Span, b: &simplicityhl::error::Span) -> bool { - a.start <= b.start && a.end >= b.end + a.file_id == b.file_id && a.start <= b.start && a.end >= b.end } /// Convert byte offset to [`lsp_types::Position`]. @@ -105,7 +105,7 @@ pub fn position_to_span( ) -> Result { let start_line = position_to_offset(position, rope)?; - Ok(simplicityhl::error::Span::new(start_line, start_line)) + Ok(simplicityhl::error::Span::new(0, start_line..start_line)) } /// Get document comments, using lines above given line index. Only used to @@ -170,10 +170,10 @@ pub fn get_comments_from_lines(line: u32, rope: &Rope) -> String { pub fn get_call_span(call: &simplicityhl::parse::Call) -> simplicityhl::error::Span { let length = call.name().to_string().len(); - simplicityhl::error::Span { - start: call.span().start, - end: call.span().start + length, - } + simplicityhl::error::Span::new( + call.span().file_id, + call.span().start..call.span().start + length, + ) } /// Find the position of a key in the JSON text @@ -364,7 +364,7 @@ impl Document { .functions() .iter() .filter_map(|func| { - let uri = self.linearization_map.get(func.file_id())?; + let uri = self.linearization_map.get(func.span().file_id)?; Some( parse::ExprTree::Expression(func.body()) .pre_order_iter() @@ -439,7 +439,7 @@ impl Document { .functions .functions() .into_iter() - .find(|func| span_contains(func.span(), &token_span) && func.file_id() == 0) + .find(|func| span_contains(func.span(), &token_span) && func.span().file_id == 0) .ok_or(LspError::CallNotFound( "Span of the call is not inside function.".into(), ))?; @@ -464,7 +464,9 @@ impl Document { /// Append functions imported via `use` declarations to [`Document`], /// respecting aliases (e.g. `use crate::a::func as func2`). pub fn populate_visible_functions(&mut self, template_program: &simplicityhl::TemplateProgram) { - let source_map = template_program.source_map(); + let Some(source_map) = template_program.source_map() else { + return; + }; // Populate linearization_map from module_registry. let mut modules: Vec<_> = source_map.iter().map(|(p, id)| (*id, p)).collect(); @@ -673,7 +675,7 @@ mod tests { let text = Rope::from_str("/// π\nfn foo() {}"); // "/// " = 4 bytes, "π" = 2 bytes, "\n" = 1 byte, so `fn` starts at byte 7. - let span = simplicityhl::error::Span::new(7, 9); + let span = simplicityhl::error::Span::new(0, 7..9); let (start, end) = span_to_positions(&span, &text).expect("span conversion should succeed"); @@ -712,7 +714,7 @@ mod tests { .expect("identifier start should map to the same byte offset"); assert_eq!(offset, 4); - assert_eq!(span, simplicityhl::error::Span::new(4, 4)); + assert_eq!(span, simplicityhl::error::Span::new(0, 4..4)); } #[test]