Make word completions less intrusive (#36745)

Introduce `min_words_query_len` threshold for automatic word completion
display, and set it to 3 by default.

Re-enable word completions in Markdown and Plaintext.

Release Notes:

- Introduced `min_words_query_len` threshold for automatic word
completion display, and set it to 3 by default to make them less
intrusive
This commit is contained in:
Kirill Bulatov 2025-08-22 16:58:17 +03:00 committed by GitHub
parent 92bbcdeb7d
commit 3d2fa72d1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 109 additions and 21 deletions

View file

@ -1503,6 +1503,11 @@
// //
// Default: fallback // Default: fallback
"words": "fallback", "words": "fallback",
// Minimum number of characters required to automatically trigger word-based completions.
// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
//
// Default: 3
"words_min_length": 3,
// Whether to fetch LSP completions or not. // Whether to fetch LSP completions or not.
// //
// Default: true // Default: true
@ -1642,9 +1647,6 @@
"use_on_type_format": false, "use_on_type_format": false,
"allow_rewrap": "anywhere", "allow_rewrap": "anywhere",
"soft_wrap": "editor_width", "soft_wrap": "editor_width",
"completions": {
"words": "disabled"
},
"prettier": { "prettier": {
"allowed": true "allowed": true
} }
@ -1658,9 +1660,6 @@
} }
}, },
"Plain Text": { "Plain Text": {
"completions": {
"words": "disabled"
},
"allow_rewrap": "anywhere" "allow_rewrap": "anywhere"
}, },
"Python": { "Python": {

View file

@ -301,6 +301,7 @@ mod tests {
init_test(cx, |settings| { init_test(cx, |settings| {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -533,6 +534,7 @@ mod tests {
init_test(cx, |settings| { init_test(cx, |settings| {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,

View file

@ -5576,6 +5576,11 @@ impl Editor {
.as_ref() .as_ref()
.is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); .is_none_or(|query| !query.chars().any(|c| c.is_digit(10)));
let omit_word_completions = match &query {
Some(query) => query.chars().count() < completion_settings.words_min_length,
None => completion_settings.words_min_length != 0,
};
let (mut words, provider_responses) = match &provider { let (mut words, provider_responses) = match &provider {
Some(provider) => { Some(provider) => {
let provider_responses = provider.completions( let provider_responses = provider.completions(
@ -5587,9 +5592,11 @@ impl Editor {
cx, cx,
); );
let words = match completion_settings.words { let words = match (omit_word_completions, completion_settings.words) {
WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), (true, _) | (_, WordsCompletionMode::Disabled) => {
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx Task::ready(BTreeMap::default())
}
(false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx
.background_spawn(async move { .background_spawn(async move {
buffer_snapshot.words_in_range(WordsQuery { buffer_snapshot.words_in_range(WordsQuery {
fuzzy_contents: None, fuzzy_contents: None,
@ -5601,16 +5608,20 @@ impl Editor {
(words, provider_responses) (words, provider_responses)
} }
None => ( None => {
cx.background_spawn(async move { let words = if omit_word_completions {
buffer_snapshot.words_in_range(WordsQuery { Task::ready(BTreeMap::default())
fuzzy_contents: None, } else {
range: word_search_range, cx.background_spawn(async move {
skip_digits, buffer_snapshot.words_in_range(WordsQuery {
fuzzy_contents: None,
range: word_search_range,
skip_digits,
})
}) })
}), };
Task::ready(Ok(Vec::new())), (words, Task::ready(Ok(Vec::new())))
), }
}; };
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;

View file

@ -12237,6 +12237,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
lsp_insert_mode, lsp_insert_mode,
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
}); });
@ -12295,6 +12296,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
update_test_language_settings(&mut cx, |settings| { update_test_language_settings(&mut cx, |settings| {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
// set the opposite here to ensure that the action is overriding the default behavior // set the opposite here to ensure that the action is overriding the default behavior
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
lsp: true, lsp: true,
@ -12331,6 +12333,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
update_test_language_settings(&mut cx, |settings| { update_test_language_settings(&mut cx, |settings| {
settings.defaults.completions = Some(CompletionSettings { settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
// set the opposite here to ensure that the action is overriding the default behavior // set the opposite here to ensure that the action is overriding the default behavior
lsp_insert_mode: LspInsertMode::Replace, lsp_insert_mode: LspInsertMode::Replace,
lsp: true, lsp: true,
@ -13072,6 +13075,7 @@ async fn test_word_completion(cx: &mut TestAppContext) {
init_test(cx, |language_settings| { init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings { language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Fallback, words: WordsCompletionMode::Fallback,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 10, lsp_fetch_timeout_ms: 10,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -13168,6 +13172,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext
init_test(cx, |language_settings| { init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings { language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Enabled, words: WordsCompletionMode::Enabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -13231,6 +13236,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
init_test(cx, |language_settings| { init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings { language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled, words: WordsCompletionMode::Disabled,
words_min_length: 0,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -13304,6 +13310,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
init_test(cx, |language_settings| { init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings { language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Fallback, words: WordsCompletionMode::Fallback,
words_min_length: 0,
lsp: false, lsp: false,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert, lsp_insert_mode: LspInsertMode::Insert,
@ -13361,6 +13368,56 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Enabled,
words_min_length: 3,
lsp: true,
lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert,
});
});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
cx.set_state(indoc! {"ˇ
wow
wowen
wowser
"});
cx.simulate_keystroke("w");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!(
"expected completion menu to be hidden, as words completion threshold is not met"
);
}
});
cx.simulate_keystroke("o");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!(
"expected completion menu to be hidden, as words completion threshold is not met still"
);
}
});
cx.simulate_keystroke("w");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
} else {
panic!("expected completion menu to be open after the word completions threshold is met");
}
});
}
fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> { fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
let position = || lsp::Position { let position = || lsp::Position {
line: params.text_document_position.position.line, line: params.text_document_position.position.line,

View file

@ -350,6 +350,12 @@ pub struct CompletionSettings {
/// Default: `fallback` /// Default: `fallback`
#[serde(default = "default_words_completion_mode")] #[serde(default = "default_words_completion_mode")]
pub words: WordsCompletionMode, pub words: WordsCompletionMode,
/// How many characters has to be in the completions query to automatically show the words-based completions.
/// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
///
/// Default: 3
#[serde(default = "default_3")]
pub words_min_length: usize,
/// Whether to fetch LSP completions or not. /// Whether to fetch LSP completions or not.
/// ///
/// Default: true /// Default: true
@ -359,7 +365,7 @@ pub struct CompletionSettings {
/// When set to 0, waits indefinitely. /// When set to 0, waits indefinitely.
/// ///
/// Default: 0 /// Default: 0
#[serde(default = "default_lsp_fetch_timeout_ms")] #[serde(default)]
pub lsp_fetch_timeout_ms: u64, pub lsp_fetch_timeout_ms: u64,
/// Controls how LSP completions are inserted. /// Controls how LSP completions are inserted.
/// ///
@ -405,8 +411,8 @@ fn default_lsp_insert_mode() -> LspInsertMode {
LspInsertMode::ReplaceSuffix LspInsertMode::ReplaceSuffix
} }
fn default_lsp_fetch_timeout_ms() -> u64 { fn default_3() -> usize {
0 3
} }
/// The settings for a particular language. /// The settings for a particular language.
@ -1468,6 +1474,7 @@ impl settings::Settings for AllLanguageSettings {
} else { } else {
d.completions = Some(CompletionSettings { d.completions = Some(CompletionSettings {
words: mode, words: mode,
words_min_length: 3,
lsp: true, lsp: true,
lsp_fetch_timeout_ms: 0, lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::ReplaceSuffix, lsp_insert_mode: LspInsertMode::ReplaceSuffix,

View file

@ -2425,6 +2425,7 @@ Examples:
{ {
"completions": { "completions": {
"words": "fallback", "words": "fallback",
"words_min_length": 3,
"lsp": true, "lsp": true,
"lsp_fetch_timeout_ms": 0, "lsp_fetch_timeout_ms": 0,
"lsp_insert_mode": "replace_suffix" "lsp_insert_mode": "replace_suffix"
@ -2444,6 +2445,17 @@ Examples:
2. `fallback` - Only if LSP response errors or times out, use document's words to show 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) 3. `disabled` - Never fetch or complete document's words for completions (word-based completions can still be queried via a separate action)
### Min Words Query Length
- Description: Minimum number of characters required to automatically trigger word-based completions.
Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
- Setting: `words_min_length`
- Default: `3`
**Options**
Positive integer values
### LSP ### LSP
- Description: Whether to fetch LSP completions or not. - Description: Whether to fetch LSP completions or not.