Detailed changes
@@ -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": {
@@ -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,
});
});
@@ -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)>>,
@@ -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
}
@@ -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);
@@ -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);
@@ -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.