diff --git a/crates/cairo-lang-language-server/src/lsp/capabilities/client.rs b/crates/cairo-lang-language-server/src/lsp/capabilities/client.rs index e8e4211aa69..1a79aa98473 100644 --- a/crates/cairo-lang-language-server/src/lsp/capabilities/client.rs +++ b/crates/cairo-lang-language-server/src/lsp/capabilities/client.rs @@ -17,6 +17,27 @@ pub trait ClientCapabilitiesExt { /// The client supports dynamic registration for text document synchronization capabilities. fn text_document_synchronization_dynamic_registration(&self) -> bool; + + /// The client supports dynamic registration for completion capabilities. + fn completion_dynamic_registration(&self) -> bool; + + /// The client supports dynamic registration for execute command capabilities. + fn execute_command_dynamic_registration(&self) -> bool; + + /// The client supports dynamic registration for semantic tokens capabilities. + fn semantic_tokens_dynamic_registration(&self) -> bool; + + /// The client supports dynamic registration for formatting capabilities. + fn formatting_dynamic_registration(&self) -> bool; + + /// The client supports dynamic registration for hover capabilities. + fn hover_dynamic_registration(&self) -> bool; + + /// The client supports dynamic registration for definition capabilities. + fn definition_dynamic_registration(&self) -> bool; + + /// The client supports dynamic registration for code action capabilities. + fn code_action_dynamic_registration(&self) -> bool; } impl ClientCapabilitiesExt for ClientCapabilities { @@ -37,4 +58,34 @@ impl ClientCapabilitiesExt for ClientCapabilities { self.text_document.as_ref()?.synchronization.as_ref()?.dynamic_registration? ) } + + fn completion_dynamic_registration(&self) -> bool { + try_or_default!(self.text_document.as_ref()?.completion.as_ref()?.dynamic_registration?) + } + + fn execute_command_dynamic_registration(&self) -> bool { + try_or_default!(self.workspace.as_ref()?.execute_command.as_ref()?.dynamic_registration?) + } + + fn semantic_tokens_dynamic_registration(&self) -> bool { + try_or_default!( + self.text_document.as_ref()?.semantic_tokens.as_ref()?.dynamic_registration? + ) + } + + fn formatting_dynamic_registration(&self) -> bool { + try_or_default!(self.text_document.as_ref()?.formatting.as_ref()?.dynamic_registration?) + } + + fn hover_dynamic_registration(&self) -> bool { + try_or_default!(self.text_document.as_ref()?.hover.as_ref()?.dynamic_registration?) + } + + fn definition_dynamic_registration(&self) -> bool { + try_or_default!(self.text_document.as_ref()?.definition.as_ref()?.dynamic_registration?) + } + + fn code_action_dynamic_registration(&self) -> bool { + try_or_default!(self.text_document.as_ref()?.code_action.as_ref()?.dynamic_registration?) + } } diff --git a/crates/cairo-lang-language-server/src/lsp/capabilities/server.rs b/crates/cairo-lang-language-server/src/lsp/capabilities/server.rs index 478a2035e69..4bdc147109a 100644 --- a/crates/cairo-lang-language-server/src/lsp/capabilities/server.rs +++ b/crates/cairo-lang-language-server/src/lsp/capabilities/server.rs @@ -1,11 +1,33 @@ +//! Module for collecting static and dynamic capabilities the server wants to register. +//! A capability can be registered statically ONLY +//! if the client does not support dynamic registration for this capability, as per LSP spec +//! : +//! +//! > Server must not register the same capability both statically through the initialize result and +//! > dynamically for the same document selector. If a server wants to support both static and +//! > dynamic +//! > registration it needs to check the client capability in the initialize request and only +//! > register +//! > the capability statically if the client doesn’t support dynamic registration for that +//! > capability. + +use std::ops::Not; + +use missing_lsp_types::{ + CodeActionRegistrationOptions, DefinitionRegistrationOptions, + DocumentFormattingRegistrationOptions, +}; +use serde::Serialize; use tower_lsp::lsp_types::{ ClientCapabilities, CodeActionProviderCapability, CompletionOptions, - DidChangeWatchedFilesRegistrationOptions, DocumentFilter, ExecuteCommandOptions, - FileSystemWatcher, GlobPattern, HoverProviderCapability, OneOf, Registration, SaveOptions, - SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, ServerCapabilities, - TextDocumentChangeRegistrationOptions, TextDocumentRegistrationOptions, - TextDocumentSaveRegistrationOptions, TextDocumentSyncCapability, TextDocumentSyncKind, - TextDocumentSyncOptions, TextDocumentSyncSaveOptions, + CompletionRegistrationOptions, DefinitionOptions, DidChangeWatchedFilesRegistrationOptions, + DocumentFilter, ExecuteCommandOptions, ExecuteCommandRegistrationOptions, FileSystemWatcher, + GlobPattern, HoverProviderCapability, HoverRegistrationOptions, OneOf, Registration, + SaveOptions, SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, + SemanticTokensRegistrationOptions, ServerCapabilities, TextDocumentChangeRegistrationOptions, + TextDocumentRegistrationOptions, TextDocumentSaveRegistrationOptions, + TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, + TextDocumentSyncSaveOptions, }; use crate::ide::semantic_highlighting::SemanticTokenKind; @@ -14,12 +36,10 @@ use crate::lsp::capabilities::client::ClientCapabilitiesExt; /// Returns capabilities the server wants to register statically. pub fn collect_server_capabilities(client_capabilities: &ClientCapabilities) -> ServerCapabilities { ServerCapabilities { - text_document_sync: if client_capabilities + text_document_sync: client_capabilities .text_document_synchronization_dynamic_registration() - { - None - } else { - Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions { + .not() + .then_some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions { open_close: Some(true), change: Some(TextDocumentSyncKind::FULL), will_save: Some(false), @@ -27,34 +47,53 @@ pub fn collect_server_capabilities(client_capabilities: &ClientCapabilities) -> save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { include_text: Some(false), })), - })) - }, - completion_provider: Some(CompletionOptions { - resolve_provider: Some(false), - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - all_commit_characters: None, - work_done_progress_options: Default::default(), - completion_item: None, - }), - execute_command_provider: Some(ExecuteCommandOptions { - commands: vec!["cairo.reload".to_string()], - work_done_progress_options: Default::default(), - }), - semantic_tokens_provider: Some( - SemanticTokensOptions { - legend: SemanticTokensLegend { - token_types: SemanticTokenKind::legend(), - token_modifiers: vec![], - }, - full: Some(SemanticTokensFullOptions::Bool(true)), - ..SemanticTokensOptions::default() - } - .into(), + })), + completion_provider: client_capabilities.completion_dynamic_registration().not().then( + || CompletionOptions { + resolve_provider: Some(false), + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + all_commit_characters: None, + work_done_progress_options: Default::default(), + completion_item: None, + }, ), - document_formatting_provider: Some(OneOf::Left(true)), - hover_provider: Some(HoverProviderCapability::Simple(true)), - definition_provider: Some(OneOf::Left(true)), - code_action_provider: Some(CodeActionProviderCapability::Simple(true)), + execute_command_provider: client_capabilities + .execute_command_dynamic_registration() + .not() + .then(|| ExecuteCommandOptions { + commands: vec!["cairo.reload".to_string()], + work_done_progress_options: Default::default(), + }), + semantic_tokens_provider: client_capabilities + .semantic_tokens_dynamic_registration() + .not() + .then(|| { + SemanticTokensOptions { + legend: SemanticTokensLegend { + token_types: SemanticTokenKind::legend(), + token_modifiers: vec![], + }, + full: Some(SemanticTokensFullOptions::Bool(true)), + ..SemanticTokensOptions::default() + } + .into() + }), + document_formatting_provider: client_capabilities + .formatting_dynamic_registration() + .not() + .then_some(OneOf::Left(true)), + hover_provider: client_capabilities + .hover_dynamic_registration() + .not() + .then_some(HoverProviderCapability::Simple(true)), + definition_provider: client_capabilities + .definition_dynamic_registration() + .not() + .then_some(OneOf::Left(true)), + code_action_provider: client_capabilities + .code_action_dynamic_registration() + .not() + .then_some(CodeActionProviderCapability::Simple(true)), ..ServerCapabilities::default() } } @@ -65,9 +104,25 @@ pub fn collect_dynamic_registrations( ) -> Vec { let mut registrations = vec![]; + // Relevant files. + let document_selector = Some(vec![ + DocumentFilter { + language: Some("cairo".to_string()), + scheme: Some("file".to_string()), + pattern: None, + }, + DocumentFilter { + language: Some("cairo".to_string()), + scheme: Some("vfs".to_string()), + pattern: None, + }, + ]); + let text_document_registration_options = + TextDocumentRegistrationOptions { document_selector: document_selector.clone() }; + if client_capabilities.did_change_watched_files_dynamic_registration() { // Register patterns for the client file watcher. - // This is used to detect changes to Scarb.toml and invalidate .cairo files. + // This is used to detect changes to config files and invalidate .cairo files. let registration_options = DidChangeWatchedFilesRegistrationOptions { watchers: ["/**/*.cairo", "/**/Scarb.toml", "/**/Scarb.lock", "/**/cairo_project.toml"] .map(|glob_pattern| FileSystemWatcher { @@ -76,66 +131,165 @@ pub fn collect_dynamic_registrations( }) .into(), }; - registrations.push(Registration { - id: "workspace/didChangeWatchedFiles".to_string(), - method: "workspace/didChangeWatchedFiles".to_string(), - register_options: Some(serde_json::to_value(registration_options).unwrap()), - }); + + registrations + .push(create_registration("workspace/didChangeWatchedFiles", registration_options)); } if client_capabilities.text_document_synchronization_dynamic_registration() { - let document_selector = Some(vec![ - DocumentFilter { - language: Some("cairo".to_string()), - scheme: Some("file".to_string()), - pattern: None, + registrations + .push(create_registration("textDocument/didOpen", &text_document_registration_options)); + + registrations.push(create_registration( + "textDocument/didChange", + TextDocumentChangeRegistrationOptions { + document_selector, + sync_kind: 1, // TextDocumentSyncKind::FULL }, - DocumentFilter { - language: Some("cairo".to_string()), - scheme: Some("vfs".to_string()), - pattern: None, + )); + + registrations.push(create_registration( + "textDocument/didSave", + TextDocumentSaveRegistrationOptions { + include_text: Some(false), + text_document_registration_options: text_document_registration_options.clone(), }, - ]); - - let text_document_registration_options = - TextDocumentRegistrationOptions { document_selector: document_selector.clone() }; - - registrations.push(Registration { - id: "textDocument/didOpen".to_string(), - method: "textDocument/didOpen".to_string(), - register_options: Some( - serde_json::to_value(&text_document_registration_options).unwrap(), - ), - }); - registrations.push(Registration { - id: "textDocument/didChange".to_string(), - method: "textDocument/didChange".to_string(), - register_options: Some( - serde_json::to_value(TextDocumentChangeRegistrationOptions { - document_selector, - sync_kind: 1, // TextDocumentSyncKind::FULL - }) - .unwrap(), - ), - }); - registrations.push(Registration { - id: "textDocument/didSave".to_string(), - method: "textDocument/didSave".to_string(), - register_options: Some( - serde_json::to_value(TextDocumentSaveRegistrationOptions { - include_text: Some(false), - text_document_registration_options: text_document_registration_options.clone(), - }) - .unwrap(), - ), - }); - registrations.push(Registration { - id: "textDocument/didClose".to_string(), - method: "textDocument/didClose".to_string(), - register_options: Some( - serde_json::to_value(&text_document_registration_options).unwrap(), - ), - }); + )); + + registrations.push(create_registration( + "textDocument/didClose", + &text_document_registration_options, + )); + } + + if client_capabilities.completion_dynamic_registration() { + let registration_options = CompletionRegistrationOptions { + text_document_registration_options: text_document_registration_options.clone(), + completion_options: CompletionOptions { + resolve_provider: Some(false), + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + all_commit_characters: None, + work_done_progress_options: Default::default(), + completion_item: None, + }, + }; + + registrations.push(create_registration("textDocument/completion", registration_options)); + } + + if client_capabilities.execute_command_dynamic_registration() { + let registration_options = ExecuteCommandRegistrationOptions { + commands: vec!["cairo.reload".to_string()], + execute_command_options: ExecuteCommandOptions { + commands: vec!["cairo.reload".to_string()], + work_done_progress_options: Default::default(), + }, + }; + + registrations.push(create_registration("workspace/executeCommand", registration_options)); + } + + if client_capabilities.semantic_tokens_dynamic_registration() { + let registration_options = SemanticTokensRegistrationOptions { + text_document_registration_options: text_document_registration_options.clone(), + semantic_tokens_options: SemanticTokensOptions { + legend: SemanticTokensLegend { + token_types: SemanticTokenKind::legend(), + token_modifiers: vec![], + }, + full: Some(SemanticTokensFullOptions::Bool(true)), + ..SemanticTokensOptions::default() + }, + static_registration_options: Default::default(), + }; + + registrations + .push(create_registration("textDocument/semanticTokens", registration_options)); + } + + if client_capabilities.formatting_dynamic_registration() { + let registration_options = DocumentFormattingRegistrationOptions { + text_document_registration_options: text_document_registration_options.clone(), + document_formatting_options: Default::default(), + }; + + registrations.push(create_registration("textDocument/formatting", registration_options)); + } + + if client_capabilities.hover_dynamic_registration() { + let registration_options = HoverRegistrationOptions { + text_document_registration_options: text_document_registration_options.clone(), + hover_options: Default::default(), + }; + + registrations.push(create_registration("textDocument/hover", registration_options)); + } + + if client_capabilities.definition_dynamic_registration() { + let registration_options = DefinitionRegistrationOptions { + text_document_registration_options: text_document_registration_options.clone(), + definition_options: DefinitionOptions { + work_done_progress_options: Default::default(), + }, + }; + + registrations.push(create_registration("textDocument/definition", registration_options)); } + + if client_capabilities.code_action_dynamic_registration() { + let registration_options = CodeActionRegistrationOptions { + text_document_registration_options, + code_action_options: Default::default(), + }; + + registrations.push(create_registration("textDocument/codeAction", registration_options)); + } + registrations } + +fn create_registration(method: &str, registration_options: impl Serialize) -> Registration { + Registration { + id: method.to_string(), + method: method.to_string(), + register_options: Some(serde_json::to_value(registration_options).unwrap()), + } +} + +mod missing_lsp_types { + use serde::{Deserialize, Serialize}; + use tower_lsp::lsp_types::{ + CodeActionOptions, DefinitionOptions, DocumentFormattingOptions, + TextDocumentRegistrationOptions, + }; + + #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct DocumentFormattingRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub document_formatting_options: DocumentFormattingOptions, + } + + #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct DefinitionRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub definition_options: DefinitionOptions, + } + + #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct CodeActionRegistrationOptions { + #[serde(flatten)] + pub text_document_registration_options: TextDocumentRegistrationOptions, + + #[serde(flatten)] + pub code_action_options: CodeActionOptions, + } +}