diff --git a/book/src/configuration.md b/book/src/configuration.md index 2af0e6326c880..a6c7eea54af5f 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -127,6 +127,7 @@ The following statusline elements can be configured: | `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` | | `display-inlay-hints` | Display inlay hints[^2] | `false` | | `display-signature-help-docs` | Display docs under signature help popup | `true` | +| `enable_snippet` | Enable snippet completions | `true` | [^1]: By default, a progress spinner is shown in the statusline beside the file path. [^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 3dab6bc559a98..d14d626248d3c 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -411,7 +411,7 @@ impl Client { // General messages // ------------------------------------------------------------------------------------------- - pub(crate) async fn initialize(&self) -> Result { + pub(crate) async fn initialize(&self, enable_snippets: bool) -> Result { if let Some(config) = &self.config { log::info!("Using custom LSP config: {}", config); } @@ -459,7 +459,7 @@ impl Client { text_document: Some(lsp::TextDocumentClientCapabilities { completion: Some(lsp::CompletionClientCapabilities { completion_item: Some(lsp::CompletionItemCapability { - snippet_support: Some(true), + snippet_support: Some(enable_snippets), resolve_support: Some(lsp::CompletionItemCapabilityResolveSupport { properties: vec![ String::from("documentation"), diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index d56148a415609..2138769ce033f 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -334,6 +334,14 @@ pub mod util { transaction.with_selection(selection) } + pub fn is_snippet(item: &lsp::CompletionItem) -> bool { + matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET)) + || matches!( + item.insert_text_format, + Some(lsp::InsertTextFormat::SNIPPET) + ) + } + /// Creates a [Transaction] from the [snippet::Snippet] in a completion response. /// The transaction applies the edit to all cursors. #[allow(clippy::too_many_arguments)] @@ -647,6 +655,7 @@ impl Registry { language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], + enable_snippets: bool, ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -661,8 +670,14 @@ impl Registry { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path, root_dirs)?; + let NewClientResult(client, incoming) = start_client( + id, + language_config, + config, + doc_path, + root_dirs, + enable_snippets, + )?; self.incoming.push(UnboundedReceiverStream::new(incoming)); let old_clients = entry.insert(vec![(id, client.clone())]); @@ -695,6 +710,7 @@ impl Registry { language_config: &LanguageConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], + enable_snippets: bool, ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, @@ -711,8 +727,14 @@ impl Registry { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let NewClientResult(client, incoming) = - start_client(id, language_config, config, doc_path, root_dirs)?; + let NewClientResult(client, incoming) = start_client( + id, + language_config, + config, + doc_path, + root_dirs, + enable_snippets, + )?; clients.push((id, client.clone())); self.incoming.push(UnboundedReceiverStream::new(incoming)); Ok(Some(client)) @@ -811,6 +833,7 @@ fn start_client( ls_config: &LanguageServerConfiguration, doc_path: Option<&std::path::PathBuf>, root_dirs: &[PathBuf], + enable_snippets: bool, ) -> Result { let (client, incoming, initialize_notify) = Client::start( &ls_config.command, @@ -834,7 +857,7 @@ fn start_client( .capabilities .get_or_try_init(|| { _client - .initialize() + .initialize(enable_snippets) .map_ok(|response| response.capabilities) }) .await; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ca55151add5ff..dd51858ea5a34 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1378,9 +1378,12 @@ fn lsp_restart( .context("LSP not defined for the current document")?; let scope = config.scope.clone(); - cx.editor - .language_servers - .restart(config, doc.path(), &editor_config.workspace_lsp_roots)?; + cx.editor.language_servers.restart( + config, + doc.path(), + &editor_config.workspace_lsp_roots, + editor_config.lsp.enable_snippets, + )?; // This collect is needed because refresh_language_server would need to re-borrow editor. let document_ids_to_refresh: Vec = cx diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index da6b5ddcbc1fb..8b91a44936f53 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -15,7 +15,10 @@ use helix_view::{graphics::Rect, Document, Editor}; use crate::commands; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; -use helix_lsp::{lsp, util}; +use helix_lsp::{ + lsp, + util::{self, is_snippet}, +}; use lsp::CompletionItem; impl menu::Item for CompletionItem { @@ -107,8 +110,12 @@ impl Completion { offset_encoding: helix_lsp::OffsetEncoding, start_offset: usize, trigger_offset: usize, + show_snippets: bool, ) -> Self { let replace_mode = editor.config().completion_replace; + if !show_snippets { + items.retain(|completion| !is_snippet(completion)) + } // Sort completion items according to their preselect status (given by the LSP server) items.sort_by_key(|item| !item.preselect.unwrap_or(false)); @@ -166,12 +173,7 @@ impl Completion { (None, new_text) }; - if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET)) - || matches!( - item.insert_text_format, - Some(lsp::InsertTextFormat::SNIPPET) - ) - { + if is_snippet(item) { match snippet::parse(&new_text) { Ok(snippet) => util::generate_transaction_from_snippet( doc.text(), diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index d4b141a041af6..8ed0e49d32b55 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -960,6 +960,7 @@ impl EditorView { offset_encoding, start_offset, trigger_offset, + editor.config().lsp.enable_snippets, ); if completion.is_empty() { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 727e1261d54c6..794fad82def04 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -352,6 +352,8 @@ pub struct LspConfig { pub display_signature_help_docs: bool, /// Display inlay hints pub display_inlay_hints: bool, + /// Whether to enable snippet support + pub enable_snippets: bool, } impl Default for LspConfig { @@ -362,6 +364,7 @@ impl Default for LspConfig { auto_signature_help: true, display_signature_help_docs: true, display_inlay_hints: false, + enable_snippets: true, } } } @@ -1092,12 +1095,18 @@ impl Editor { // if doc doesn't have a URL it's a scratch buffer, ignore it let doc = self.document(doc_id)?; let (lang, path) = (doc.language.clone(), doc.path().cloned()); - let root_dirs = &doc.config.load().workspace_lsp_roots; + let config = doc.config.load(); + let root_dirs = &config.workspace_lsp_roots; // try to find a language server based on the language name let language_server = lang.as_ref().and_then(|language| { self.language_servers - .get(language, path.as_ref(), root_dirs) + .get( + language, + path.as_ref(), + root_dirs, + config.lsp.enable_snippets, + ) .map_err(|e| { log::error!( "Failed to initialize the LSP for `{}` {{ {} }}",