Add `completions.lsp_insert_mode` setting to control what ranges are replaced when a completion is inserted (#27453)

frederik-uni , João Marcos , and Max Brunsfeld created

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 <marcospb19@hotmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

assets/settings/default.json                      |  22 +
crates/copilot/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(-)

Detailed changes

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": {

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,
             });
         });
 

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 <edi|> 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 <edi|tor> 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 <tor|> 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: "<p|field>: 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: "[<element_|element_2>]".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: "[<el|element>]".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: "<Sub|Error>".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: "<Sub|Err>".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: "<Su|scrirr>".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(<ind|ix>)".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<AtomicUsize>,
+) -> impl Future<Output = ()> {
+    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::<lsp::request::Completion, _, _>(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<Vec<(&'static str, &'static str)>>,

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<LanguageName, LanguageSettings>,
     pub(crate) file_types: HashMap<Arc<str>, 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
 }

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<Anchor>, 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::<String>();
+                        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);

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<RefCell<Box<[Completion]>>>,
         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);

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.