Support word-based completions (#26410)
Closes https://github.com/zed-industries/zed/issues/4957 https://github.com/user-attachments/assets/ff491378-376d-48ec-b552-6cc80f74200b Adds `"completions"` language settings section, to configure LSP and word completions per language. Word-based completions may be turned on never, always (returned along with the LSP ones), and as a fallback if no LSP completion items were returned. Future work: * words are matched with the same fuzzy matching code that the rest of the completions are This might worsen the completion menu's usability even more, and will require work on better completion sorting. * completion entries currently have no icons or other ways to indicate those are coming from LSP or from word search, or from something else * we may work with language scopes more intelligently, group words by them and distinguish during completions Release Notes: - Supported word-based completions --------- Co-authored-by: Max Brunsfeld <max@zed.dev>
This commit is contained in:
parent
74c29f1818
commit
91c209900b
10 changed files with 632 additions and 102 deletions
|
@ -101,6 +101,7 @@ use itertools::Itertools;
|
|||
use language::{
|
||||
language_settings::{
|
||||
self, all_language_settings, language_settings, InlayHintSettings, RewrapBehavior,
|
||||
WordsCompletionMode,
|
||||
},
|
||||
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
|
||||
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, EditPredictionsMode,
|
||||
|
@ -4012,9 +4013,8 @@ impl Editor {
|
|||
} else {
|
||||
return;
|
||||
};
|
||||
let show_completion_documentation = buffer
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let show_completion_documentation = buffer_snapshot
|
||||
.settings_at(buffer_position, cx)
|
||||
.show_completion_documentation;
|
||||
|
||||
|
@ -4038,6 +4038,51 @@ impl Editor {
|
|||
};
|
||||
let completions =
|
||||
provider.completions(&buffer, buffer_position, completion_context, window, cx);
|
||||
let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
|
||||
let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
|
||||
let word_to_exclude = buffer_snapshot
|
||||
.text_for_range(old_range.clone())
|
||||
.collect::<String>();
|
||||
(
|
||||
buffer_snapshot.anchor_before(old_range.start)
|
||||
..buffer_snapshot.anchor_after(old_range.end),
|
||||
Some(word_to_exclude),
|
||||
)
|
||||
} else {
|
||||
(buffer_position..buffer_position, None)
|
||||
};
|
||||
|
||||
let completion_settings = language_settings(
|
||||
buffer_snapshot
|
||||
.language_at(buffer_position)
|
||||
.map(|language| language.name()),
|
||||
buffer_snapshot.file(),
|
||||
cx,
|
||||
)
|
||||
.completions;
|
||||
|
||||
// The document can be large, so stay in reasonable bounds when searching for words,
|
||||
// otherwise completion pop-up might be slow to appear.
|
||||
const WORD_LOOKUP_ROWS: u32 = 5_000;
|
||||
let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row;
|
||||
let min_word_search = buffer_snapshot.clip_point(
|
||||
Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0),
|
||||
Bias::Left,
|
||||
);
|
||||
let max_word_search = buffer_snapshot.clip_point(
|
||||
Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()),
|
||||
Bias::Right,
|
||||
);
|
||||
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
|
||||
..buffer_snapshot.point_to_offset(max_word_search);
|
||||
let words = match completion_settings.words {
|
||||
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
|
||||
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => {
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(None, word_search_range)
|
||||
})
|
||||
}
|
||||
};
|
||||
let sort_completions = provider.sort_completions();
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
|
@ -4046,8 +4091,55 @@ impl Editor {
|
|||
editor.update(&mut cx, |this, _| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
})?;
|
||||
let completions = completions.await.log_err();
|
||||
let menu = if let Some(completions) = completions {
|
||||
let mut completions = completions.await.log_err().unwrap_or_default();
|
||||
|
||||
match completion_settings.words {
|
||||
WordsCompletionMode::Enabled => {
|
||||
completions.extend(
|
||||
words
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
|
||||
.map(|(word, word_range)| Completion {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
confirm: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
WordsCompletionMode::Fallback => {
|
||||
if completions.is_empty() {
|
||||
completions.extend(
|
||||
words
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
|
||||
.map(|(word, word_range)| Completion {
|
||||
old_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
confirm: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
WordsCompletionMode::Disabled => {}
|
||||
}
|
||||
|
||||
let menu = if completions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut menu = CompletionsMenu::new(
|
||||
id,
|
||||
sort_completions,
|
||||
|
@ -4061,8 +4153,6 @@ impl Editor {
|
|||
.await;
|
||||
|
||||
menu.visible().then_some(menu)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
|
|
|
@ -16,7 +16,8 @@ use gpui::{
|
|||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{
|
||||
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
|
||||
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||
LanguageSettingsContent, PrettierSettings,
|
||||
},
|
||||
BracketPairConfig,
|
||||
Capability::ReadWrite,
|
||||
|
@ -30,7 +31,7 @@ use pretty_assertions::{assert_eq, assert_ne};
|
|||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
use project::FakeFs;
|
||||
use serde_json::{self, json};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
|
||||
use std::{
|
||||
iter,
|
||||
sync::atomic::{self, AtomicUsize},
|
||||
|
@ -9169,6 +9170,101 @@ async fn test_completion(cx: &mut TestAppContext) {
|
|||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_words_completion(cx: &mut TestAppContext) {
|
||||
let lsp_fetch_timeout_ms = 10;
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.completions = Some(CompletionSettings {
|
||||
words: WordsCompletionMode::Fallback,
|
||||
lsp: true,
|
||||
lsp_fetch_timeout_ms: 10,
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let throttle_completions = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let lsp_throttle_completions = throttle_completions.clone();
|
||||
let _completion_requests_handler =
|
||||
cx.lsp
|
||||
.server
|
||||
.on_request::<lsp::request::Completion, _, _>(move |_, cx| {
|
||||
let lsp_throttle_completions = lsp_throttle_completions.clone();
|
||||
async move {
|
||||
if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
|
||||
.await;
|
||||
}
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "first".into(),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "last".into(),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
}
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
oneˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(
|
||||
completion_menu_entries(&menu),
|
||||
&["first", "last"],
|
||||
"When LSP server is fast to reply, no fallback word completions are used"
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
editor.cancel(&Cancel, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| !editor.context_menu_visible())
|
||||
.await;
|
||||
|
||||
throttle_completions.store(true, atomic::Ordering::Release);
|
||||
cx.simulate_keystroke(".");
|
||||
cx.executor()
|
||||
.advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
|
||||
cx.executor().run_until_parked();
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
|
||||
"When LSP server is slow, document words can be shown instead, if configured accordingly");
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiline_completion(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue