From 07a77792c5146ab917843a3208bc70dce2f13d11 Mon Sep 17 00:00:00 2001 From: frederik-uni <147479464+frederik-uni@users.noreply.github.com> Date: Wed, 2 Apr 2025 21:55:03 +0200 Subject: [PATCH] Add `completions.lsp_insert_mode` setting to control what ranges are replaced when a completion is inserted (#27453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds `completions.lsp_insert_mode` and effectively changes the default from `"replace"` to `"replace_suffix"`, which automatically detects whether to use the LSP `replace` range instead of `insert` range. `"replace_suffix"` was chosen as a default because it's more conservative than `"replace_subsequence"`, considering that deleting text is usually faster and less disruptive than having to rewrite a long replaced word. Fixes #27197 Fixes #23395 (again) Fixes #4816 (again) Release Notes: - Added new setting `completions.lsp_insert_mode` that changes what will be replaced when an LSP completion is accepted. The default is `"replace_suffix"`, but it accepts 4 values: `"insert"` for replacing only the text before the cursor, `"replace"` for replacing the whole text, `"replace_suffix"` that acts like `"replace"` when the text after the cursor is a suffix of the completion, and `"replace_subsequence"` that acts like `"replace"` when the text around your cursor is a subsequence of the completion (similiar to a fuzzy match). Check [the documentation](https://zed.dev/docs/configuring-zed#LSP-Insert-Mode) for more information. --------- Co-authored-by: João Marcos Co-authored-by: Max Brunsfeld --- assets/settings/default.json | 22 +- .../src/copilot_completion_provider.rs | 4 +- crates/editor/src/editor_tests.rs | 270 +++++++++++++++++- crates/language/src/language_settings.rs | 26 +- crates/project/src/lsp_command.rs | 69 ++++- crates/project/src/lsp_store.rs | 27 +- docs/src/configuring-zed.md | 62 ++++ 7 files changed, 467 insertions(+), 13 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index c09c45f025..73a8ca360a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1201,7 +1201,27 @@ // When set to 0, waits indefinitely. // // Default: 0 - "lsp_fetch_timeout_ms": 0 + "lsp_fetch_timeout_ms": 0, + // Controls what range to replace when accepting LSP completions. + // + // When LSP servers give an `InsertReplaceEdit` completion, they provides two ranges: `insert` and `replace`. Usually, `insert` + // contains the word prefix before your cursor and `replace` contains the whole word. + // + // Effectively, this setting just changes whether Zed will use the received range for `insert` or `replace`, so the results may + // differ depending on the underlying LSP server. + // + // Possible values: + // 1. "insert" + // Replaces text before the cursor, using the `insert` range described in the LSP specification. + // 2. "replace" + // Replaces text before and after the cursor, using the `replace` range described in the LSP specification. + // 3. "replace_subsequence" + // Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text, + // and like `"insert"` otherwise. + // 4. "replace_suffix" + // Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like + // `"insert"` otherwise. + "lsp_insert_mode": "replace_suffix" }, // Different settings for specific languages. "languages": { diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 6c06b56005..ff63617875 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -273,7 +273,7 @@ mod tests { use language::{ Point, language_settings::{ - AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, + AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, LspInsertMode, WordsCompletionMode, }, }; @@ -294,6 +294,7 @@ mod tests { words: WordsCompletionMode::Disabled, lsp: true, lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, }); }); @@ -525,6 +526,7 @@ mod tests { words: WordsCompletionMode::Disabled, lsp: true, lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, }); }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 61870d0fb8..ae222321b3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -23,7 +23,7 @@ use language::{ Override, Point, language_settings::{ AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, - LanguageSettingsContent, PrettierSettings, + LanguageSettingsContent, LspInsertMode, PrettierSettings, }, }; use language_settings::{Formatter, FormatterList, IndentGuideSettings}; @@ -6382,7 +6382,7 @@ async fn test_autoindent_selections(cx: &mut TestAppContext) { cx.run_until_parked(); cx.update(|_, cx| { - pretty_assertions::assert_eq!( + assert_eq!( buffer.read(cx).text(), indoc! { " impl A { @@ -9198,6 +9198,203 @@ async fn test_signature_help(cx: &mut TestAppContext) { .await; } +#[gpui::test] +async fn test_completion_mode(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + struct Run { + run_description: &'static str, + initial_state: String, + buffer_marked_text: String, + completion_text: &'static str, + expected_with_insertion_mode: String, + expected_with_replace_mode: String, + expected_with_replace_subsequence_mode: String, + expected_with_replace_suffix_mode: String, + } + + let runs = [ + Run { + run_description: "Start of word matches completion text", + initial_state: "before ediˇ after".into(), + buffer_marked_text: "before after".into(), + completion_text: "editor", + expected_with_insertion_mode: "before editorˇ after".into(), + expected_with_replace_mode: "before editorˇ after".into(), + expected_with_replace_subsequence_mode: "before editorˇ after".into(), + expected_with_replace_suffix_mode: "before editorˇ after".into(), + }, + Run { + run_description: "Accept same text at the middle of the word", + initial_state: "before ediˇtor after".into(), + buffer_marked_text: "before after".into(), + completion_text: "editor", + expected_with_insertion_mode: "before editorˇtor after".into(), + expected_with_replace_mode: "before ediˇtor after".into(), + expected_with_replace_subsequence_mode: "before ediˇtor after".into(), + expected_with_replace_suffix_mode: "before ediˇtor after".into(), + }, + Run { + run_description: "End of word matches completion text -- cursor at end", + initial_state: "before torˇ after".into(), + buffer_marked_text: "before after".into(), + completion_text: "editor", + expected_with_insertion_mode: "before editorˇ after".into(), + expected_with_replace_mode: "before editorˇ after".into(), + expected_with_replace_subsequence_mode: "before editorˇ after".into(), + expected_with_replace_suffix_mode: "before editorˇ after".into(), + }, + Run { + run_description: "End of word matches completion text -- cursor at start", + initial_state: "before ˇtor after".into(), + buffer_marked_text: "before <|tor> after".into(), + completion_text: "editor", + expected_with_insertion_mode: "before editorˇtor after".into(), + expected_with_replace_mode: "before editorˇ after".into(), + expected_with_replace_subsequence_mode: "before editorˇ after".into(), + expected_with_replace_suffix_mode: "before editorˇ after".into(), + }, + Run { + run_description: "Prepend text containing whitespace", + initial_state: "pˇfield: bool".into(), + buffer_marked_text: ": bool".into(), + completion_text: "pub ", + expected_with_insertion_mode: "pub ˇfield: bool".into(), + expected_with_replace_mode: "pub ˇ: bool".into(), + expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(), + expected_with_replace_suffix_mode: "pub ˇfield: bool".into(), + }, + Run { + run_description: "Add element to start of list", + initial_state: "[element_ˇelement_2]".into(), + buffer_marked_text: "[]".into(), + completion_text: "element_1", + expected_with_insertion_mode: "[element_1ˇelement_2]".into(), + expected_with_replace_mode: "[element_1ˇ]".into(), + expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(), + expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(), + }, + Run { + run_description: "Add element to start of list -- first and second elements are equal", + initial_state: "[elˇelement]".into(), + buffer_marked_text: "[]".into(), + completion_text: "element", + expected_with_insertion_mode: "[elementˇelement]".into(), + expected_with_replace_mode: "[elˇement]".into(), + expected_with_replace_subsequence_mode: "[elementˇelement]".into(), + expected_with_replace_suffix_mode: "[elˇement]".into(), + }, + Run { + run_description: "Ends with matching suffix", + initial_state: "SubˇError".into(), + buffer_marked_text: "".into(), + completion_text: "SubscriptionError", + expected_with_insertion_mode: "SubscriptionErrorˇError".into(), + expected_with_replace_mode: "SubscriptionErrorˇ".into(), + expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(), + expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(), + }, + Run { + run_description: "Suffix is a subsequence -- contiguous", + initial_state: "SubˇErr".into(), + buffer_marked_text: "".into(), + completion_text: "SubscriptionError", + expected_with_insertion_mode: "SubscriptionErrorˇErr".into(), + expected_with_replace_mode: "SubscriptionErrorˇ".into(), + expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(), + expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(), + }, + Run { + run_description: "Suffix is a subsequence -- non-contiguous -- replace intended", + initial_state: "Suˇscrirr".into(), + buffer_marked_text: "".into(), + completion_text: "SubscriptionError", + expected_with_insertion_mode: "SubscriptionErrorˇscrirr".into(), + expected_with_replace_mode: "SubscriptionErrorˇ".into(), + expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(), + expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(), + }, + Run { + run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended", + initial_state: "foo(indˇix)".into(), + buffer_marked_text: "foo()".into(), + completion_text: "node_index", + expected_with_insertion_mode: "foo(node_indexˇix)".into(), + expected_with_replace_mode: "foo(node_indexˇ)".into(), + expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(), + expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(), + }, + ]; + + for run in runs { + let run_variations = [ + (LspInsertMode::Insert, run.expected_with_insertion_mode), + (LspInsertMode::Replace, run.expected_with_replace_mode), + ( + LspInsertMode::ReplaceSubsequence, + run.expected_with_replace_subsequence_mode, + ), + ( + LspInsertMode::ReplaceSuffix, + run.expected_with_replace_suffix_mode, + ), + ]; + + for (lsp_insert_mode, expected_text) in run_variations { + eprintln!( + "run = {:?}, mode = {lsp_insert_mode:.?}", + run.run_description, + ); + + update_test_language_settings(&mut cx, |settings| { + settings.defaults.completions = Some(CompletionSettings { + lsp_insert_mode, + words: WordsCompletionMode::Disabled, + lsp: true, + lsp_fetch_timeout_ms: 0, + }); + }); + + cx.set_state(&run.initial_state); + cx.update_editor(|editor, window, cx| { + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); + }); + + let counter = Arc::new(AtomicUsize::new(0)); + handle_completion_request_with_insert_and_replace( + &mut cx, + &run.buffer_marked_text, + vec![run.completion_text], + counter.clone(), + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + assert_eq!(counter.load(atomic::Ordering::Acquire), 1); + + let apply_additional_edits = cx.update_editor(|editor, window, cx| { + editor + .confirm_completion(&ConfirmCompletion::default(), window, cx) + .unwrap() + }); + cx.assert_editor_state(&expected_text); + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); + } + } +} + #[gpui::test] async fn test_completion(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -9419,6 +9616,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { words: WordsCompletionMode::Fallback, lsp: true, lsp_fetch_timeout_ms: 10, + lsp_insert_mode: LspInsertMode::Insert, }); }); @@ -9514,6 +9712,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext words: WordsCompletionMode::Enabled, lsp: true, lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, }); }); @@ -9576,6 +9775,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { words: WordsCompletionMode::Disabled, lsp: true, lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, }); }); @@ -9648,6 +9848,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { words: WordsCompletionMode::Fallback, lsp: false, lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, }); }); @@ -18482,7 +18683,10 @@ pub fn handle_signature_help_request( /// Handle completion request passing a marked string specifying where the completion /// should be triggered from using '|' character, what range should be replaced, and what completions -/// should be returned using '<' and '>' to delimit the range +/// should be returned using '<' and '>' to delimit the range. +/// +/// Also see `handle_completion_request_with_insert_and_replace`. +#[track_caller] pub fn handle_completion_request( cx: &mut EditorLspTestContext, marked_string: &str, @@ -18532,6 +18736,66 @@ pub fn handle_completion_request( } } +/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be +/// given instead, which also contains an `insert` range. +/// +/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range, +/// that is, `replace_range.start..cursor_pos`. +pub fn handle_completion_request_with_insert_and_replace( + cx: &mut EditorLspTestContext, + marked_string: &str, + completions: Vec<&'static str>, + counter: Arc, +) -> impl Future { + let complete_from_marker: TextRangeMarker = '|'.into(); + let replace_range_marker: TextRangeMarker = ('<', '>').into(); + let (_, mut marked_ranges) = marked_text_ranges_by( + marked_string, + vec![complete_from_marker.clone(), replace_range_marker.clone()], + ); + + let complete_from_position = + cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start); + let replace_range = + cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); + + let mut request = + cx.set_request_handler::(move |url, params, _| { + let completions = completions.clone(); + counter.fetch_add(1, atomic::Ordering::Release); + async move { + assert_eq!(params.text_document_position.text_document.uri, url.clone()); + assert_eq!( + params.text_document_position.position, complete_from_position, + "marker `|` position doesn't match", + ); + Ok(Some(lsp::CompletionResponse::Array( + completions + .iter() + .map(|completion_text| lsp::CompletionItem { + label: completion_text.to_string(), + text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { + insert: lsp::Range { + start: replace_range.start, + end: complete_from_position, + }, + replace: replace_range, + new_text: completion_text.to_string(), + }, + )), + ..Default::default() + }) + .collect(), + ))) + } + }); + + async move { + request.next().await; + } +} + fn handle_resolve_completion_request( cx: &mut EditorLspTestContext, edits: Option>, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 33e8ff4519..1887f59826 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -61,7 +61,7 @@ pub fn all_language_settings<'a>( pub struct AllLanguageSettings { /// The edit prediction settings. pub edit_predictions: EditPredictionSettings, - defaults: LanguageSettings, + pub defaults: LanguageSettings, languages: HashMap, pub(crate) file_types: HashMap, GlobSet>, } @@ -329,6 +329,11 @@ pub struct CompletionSettings { /// Default: 0 #[serde(default = "default_lsp_fetch_timeout_ms")] pub lsp_fetch_timeout_ms: u64, + /// Controls how LSP completions are inserted. + /// + /// Default: "replace_suffix" + #[serde(default = "default_lsp_insert_mode")] + pub lsp_insert_mode: LspInsertMode, } /// Controls how document's words are completed. @@ -345,10 +350,29 @@ pub enum WordsCompletionMode { Disabled, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum LspInsertMode { + /// Replaces text before the cursor, using the `insert` range described in the LSP specification. + Insert, + /// Replaces text before and after the cursor, using the `replace` range described in the LSP specification. + Replace, + /// Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text, + /// and like `"insert"` otherwise. + ReplaceSubsequence, + /// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like + /// `"insert"` otherwise. + ReplaceSuffix, +} + fn default_words_completion_mode() -> WordsCompletionMode { WordsCompletionMode::Fallback } +fn default_lsp_insert_mode() -> LspInsertMode { + LspInsertMode::Insert +} + fn default_lsp_fetch_timeout_ms() -> u64 { 0 } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index f10a7b7e5f..a4a6cbc5c8 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -17,7 +17,9 @@ use gpui::{App, AsyncApp, Entity}; use language::{ Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, - language_settings::{InlayHintKind, LanguageSettings, language_settings}, + language_settings::{ + AllLanguageSettings, InlayHintKind, LanguageSettings, LspInsertMode, language_settings, + }, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, @@ -28,6 +30,7 @@ use lsp::{ LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions, ServerCapabilities, }; +use settings::Settings as _; use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature}; use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc}; use text::{BufferId, LineEnding}; @@ -2085,7 +2088,7 @@ impl LspCommand for GetCompletions { .map(Arc::new); let mut completion_edits = Vec::new(); - buffer.update(&mut cx, |buffer, _cx| { + buffer.update(&mut cx, |buffer, cx| { let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); @@ -2122,7 +2125,16 @@ impl LspCommand for GetCompletions { // If the language server provides a range to overwrite, then // check that the range is valid. Some(completion_text_edit) => { - match parse_completion_text_edit(&completion_text_edit, &snapshot) { + let completion_mode = AllLanguageSettings::get_global(cx) + .defaults + .completions + .lsp_insert_mode; + + match parse_completion_text_edit( + &completion_text_edit, + &snapshot, + completion_mode, + ) { Some(edit) => edit, None => return false, } @@ -2303,6 +2315,7 @@ impl LspCommand for GetCompletions { pub(crate) fn parse_completion_text_edit( edit: &lsp::CompletionTextEdit, snapshot: &BufferSnapshot, + completion_mode: LspInsertMode, ) -> Option<(Range, String)> { match edit { lsp::CompletionTextEdit::Edit(edit) => { @@ -2321,7 +2334,55 @@ pub(crate) fn parse_completion_text_edit( } lsp::CompletionTextEdit::InsertAndReplace(edit) => { - let range = range_from_lsp(edit.replace); + let replace = match completion_mode { + LspInsertMode::Insert => false, + LspInsertMode::Replace => true, + LspInsertMode::ReplaceSubsequence => { + let range_to_replace = range_from_lsp(edit.replace); + + let start = snapshot.clip_point_utf16(range_to_replace.start, Bias::Left); + let end = snapshot.clip_point_utf16(range_to_replace.end, Bias::Left); + if start != range_to_replace.start.0 || end != range_to_replace.end.0 { + false + } else { + let mut completion_text = edit.new_text.chars(); + + let mut text_to_replace = snapshot.chars_for_range( + snapshot.anchor_before(start)..snapshot.anchor_after(end), + ); + + // is `text_to_replace` a subsequence of `completion_text` + text_to_replace.all(|needle_ch| { + completion_text.any(|haystack_ch| haystack_ch == needle_ch) + }) + } + } + LspInsertMode::ReplaceSuffix => { + let range_after_cursor = lsp::Range { + start: edit.insert.end, + end: edit.replace.end, + }; + let range_after_cursor = range_from_lsp(range_after_cursor); + + let start = snapshot.clip_point_utf16(range_after_cursor.start, Bias::Left); + let end = snapshot.clip_point_utf16(range_after_cursor.end, Bias::Left); + if start != range_after_cursor.start.0 || end != range_after_cursor.end.0 { + false + } else { + let text_after_cursor = snapshot + .text_for_range( + snapshot.anchor_before(start)..snapshot.anchor_after(end), + ) + .collect::(); + edit.new_text.ends_with(&text_after_cursor) + } + } + }; + + let range = range_from_lsp(match replace { + true => edit.replace, + false => edit.insert, + }); let start = snapshot.clip_point_utf16(range.start, Bias::Left); let end = snapshot.clip_point_utf16(range.end, Bias::Left); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 362d5d57b3..24e0c80a12 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -39,7 +39,8 @@ use language::{ LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, language_settings::{ - FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, + AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, LspInsertMode, + SelectedFormatter, language_settings, }, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, @@ -5151,6 +5152,7 @@ impl LspStore { &buffer_snapshot, completions.clone(), completion_index, + cx, ) .await .log_err() @@ -5184,6 +5186,7 @@ impl LspStore { snapshot: &BufferSnapshot, completions: Rc>>, completion_index: usize, + cx: &mut AsyncApp, ) -> Result<()> { let server_id = server.server_id(); let can_resolve = server @@ -5226,7 +5229,15 @@ impl LspStore { // language server we currently use that does update `text_edit` in `completionItem/resolve` // is `typescript-language-server` and they only update `text_edit.new_text`. // But we should not rely on that. - let edit = parse_completion_text_edit(text_edit, snapshot); + let completion_mode = cx + .read_global(|_: &SettingsStore, cx| { + AllLanguageSettings::get_global(cx) + .defaults + .completions + .lsp_insert_mode + }) + .unwrap_or(LspInsertMode::Insert); + let edit = parse_completion_text_edit(text_edit, snapshot, completion_mode); if let Some((old_range, mut new_text)) = edit { LineEnding::normalize(&mut new_text); @@ -5482,6 +5493,7 @@ impl LspStore { &snapshot, completions.clone(), completion_index, + cx, ) .await .context("resolving completion")?; @@ -7723,7 +7735,16 @@ impl LspStore { })??; if let Some(text_edit) = completion.text_edit.as_ref() { - let edit = parse_completion_text_edit(text_edit, &buffer_snapshot); + let completion_mode = cx + .read_global(|_: &SettingsStore, cx| { + AllLanguageSettings::get_global(cx) + .defaults + .completions + .lsp_insert_mode + }) + .unwrap_or(LspInsertMode::Insert); + + let edit = parse_completion_text_edit(text_edit, &buffer_snapshot, completion_mode); if let Some((old_range, mut text_edit_new_text)) = edit { LineEnding::normalize(&mut text_edit_new_text); diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index a7d6db2001..7045eca525 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2051,6 +2051,68 @@ Examples: `boolean` values +## Completions + +- Description: Controls how completions are processed for this language. +- Setting: `completions` +- Default: + +```json +{ + "completions": { + "words": "fallback", + "lsp": true, + "lsp_fetch_timeout_ms": 0, + "lsp_insert_mode": "replace_suffix" + } +} +``` + +### Words + +- Description: Controls how words are completed. For large documents, not all words may be fetched for completion. +- Setting: `words` +- Default: `fallback` + +**Options** + +1. `enabled` - Always fetch document's words for completions along with LSP completions +2. `fallback` - Only if LSP response errors or times out, use document's words to show completions +3. `disabled` - Never fetch or complete document's words for completions (word-based completions can still be queried via a separate action) + +### LSP + +- Description: Whether to fetch LSP completions or not. +- Setting: `lsp` +- Default: `true` + +**Options** + +`boolean` values + +### LSP Fetch Timeout (ms) + +- Description: When fetching LSP completions, determines how long to wait for a response of a particular server. When set to 0, waits indefinitely. +- Setting: `lsp_fetch_timeout_ms` +- Default: `0` + +**Options** + +`integer` values representing milliseconds + +### LSP Insert Mode + +- Description: Controls what range to replace when accepting LSP completions. +- Setting: `lsp_insert_mode` +- Default: `replace_suffix` + +**Options** + +1. `insert` - Replaces text before the cursor, using the `insert` range described in the LSP specification +2. `replace` - Replaces text before and after the cursor, using the `replace` range described in the LSP specification +3. `replace_subsequence` - Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text, and like `"insert"` otherwise +4. `replace_suffix` - Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like `"insert"` otherwise + ## Show Completions On Input - Description: Whether or not to show completions as you type.