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
|
@ -336,14 +336,14 @@
|
||||||
"active_line_width": 1,
|
"active_line_width": 1,
|
||||||
// Determines how indent guides are colored.
|
// Determines how indent guides are colored.
|
||||||
// This setting can take the following three values:
|
// This setting can take the following three values:
|
||||||
///
|
//
|
||||||
// 1. "disabled"
|
// 1. "disabled"
|
||||||
// 2. "fixed"
|
// 2. "fixed"
|
||||||
// 3. "indent_aware"
|
// 3. "indent_aware"
|
||||||
"coloring": "fixed",
|
"coloring": "fixed",
|
||||||
// Determines how indent guide backgrounds are colored.
|
// Determines how indent guide backgrounds are colored.
|
||||||
// This setting can take the following two values:
|
// This setting can take the following two values:
|
||||||
///
|
//
|
||||||
// 1. "disabled"
|
// 1. "disabled"
|
||||||
// 2. "indent_aware"
|
// 2. "indent_aware"
|
||||||
"background_coloring": "disabled"
|
"background_coloring": "disabled"
|
||||||
|
@ -402,8 +402,8 @@
|
||||||
// Time to wait after scrolling the buffer, before requesting the hints,
|
// Time to wait after scrolling the buffer, before requesting the hints,
|
||||||
// set to 0 to disable debouncing.
|
// set to 0 to disable debouncing.
|
||||||
"scroll_debounce_ms": 50,
|
"scroll_debounce_ms": 50,
|
||||||
/// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
|
// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
|
||||||
/// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
|
// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
|
||||||
"toggle_on_modifiers_press": {
|
"toggle_on_modifiers_press": {
|
||||||
"control": false,
|
"control": false,
|
||||||
"shift": false,
|
"shift": false,
|
||||||
|
@ -440,7 +440,7 @@
|
||||||
"scrollbar": {
|
"scrollbar": {
|
||||||
// When to show the scrollbar in the project panel.
|
// When to show the scrollbar in the project panel.
|
||||||
// This setting can take five values:
|
// This setting can take five values:
|
||||||
///
|
//
|
||||||
// 1. null (default): Inherit editor settings
|
// 1. null (default): Inherit editor settings
|
||||||
// 2. Show the scrollbar if there's important information or
|
// 2. Show the scrollbar if there's important information or
|
||||||
// follow the system's configured behavior (default):
|
// follow the system's configured behavior (default):
|
||||||
|
@ -455,7 +455,7 @@
|
||||||
},
|
},
|
||||||
// Which files containing diagnostic errors/warnings to mark in the project panel.
|
// Which files containing diagnostic errors/warnings to mark in the project panel.
|
||||||
// This setting can take the following three values:
|
// This setting can take the following three values:
|
||||||
///
|
//
|
||||||
// 1. Do not mark any files:
|
// 1. Do not mark any files:
|
||||||
// "off"
|
// "off"
|
||||||
// 2. Only mark files with errors:
|
// 2. Only mark files with errors:
|
||||||
|
@ -512,7 +512,7 @@
|
||||||
"scrollbar": {
|
"scrollbar": {
|
||||||
// When to show the scrollbar in the project panel.
|
// When to show the scrollbar in the project panel.
|
||||||
// This setting can take five values:
|
// This setting can take five values:
|
||||||
///
|
//
|
||||||
// 1. null (default): Inherit editor settings
|
// 1. null (default): Inherit editor settings
|
||||||
// 2. Show the scrollbar if there's important information or
|
// 2. Show the scrollbar if there's important information or
|
||||||
// follow the system's configured behavior (default):
|
// follow the system's configured behavior (default):
|
||||||
|
@ -686,7 +686,7 @@
|
||||||
// Which files containing diagnostic errors/warnings to mark in the tabs.
|
// Which files containing diagnostic errors/warnings to mark in the tabs.
|
||||||
// Diagnostics are only shown when file icons are also active.
|
// Diagnostics are only shown when file icons are also active.
|
||||||
// This setting only works when can take the following three values:
|
// This setting only works when can take the following three values:
|
||||||
///
|
//
|
||||||
// 1. Do not mark any files:
|
// 1. Do not mark any files:
|
||||||
// "off"
|
// "off"
|
||||||
// 2. Only mark files with errors:
|
// 2. Only mark files with errors:
|
||||||
|
@ -1014,7 +1014,7 @@
|
||||||
"scrollbar": {
|
"scrollbar": {
|
||||||
// When to show the scrollbar in the terminal.
|
// When to show the scrollbar in the terminal.
|
||||||
// This setting can take five values:
|
// This setting can take five values:
|
||||||
///
|
//
|
||||||
// 1. null (default): Inherit editor settings
|
// 1. null (default): Inherit editor settings
|
||||||
// 2. Show the scrollbar if there's important information or
|
// 2. Show the scrollbar if there's important information or
|
||||||
// follow the system's configured behavior (default):
|
// follow the system's configured behavior (default):
|
||||||
|
@ -1085,6 +1085,31 @@
|
||||||
"auto_install_extensions": {
|
"auto_install_extensions": {
|
||||||
"html": true
|
"html": true
|
||||||
},
|
},
|
||||||
|
// Controls how completions are processed for this language.
|
||||||
|
"completions": {
|
||||||
|
// Controls how words are completed.
|
||||||
|
// For large documents, not all words may be fetched for completion.
|
||||||
|
//
|
||||||
|
// May take 3 values:
|
||||||
|
// 1. "enabled"
|
||||||
|
// Always fetch document's words for completions.
|
||||||
|
// 2. "fallback"
|
||||||
|
// Only if LSP response errors/times out/is empty, use document's words to show completions.
|
||||||
|
// 3. "disabled"
|
||||||
|
// Never fetch or complete document's words for completions.
|
||||||
|
//
|
||||||
|
// Default: fallback
|
||||||
|
"words": "fallback",
|
||||||
|
// Whether to fetch LSP completions or not.
|
||||||
|
//
|
||||||
|
// Default: true
|
||||||
|
"lsp": true,
|
||||||
|
// When fetching LSP completions, determines how long to wait for a response of a particular server.
|
||||||
|
// When set to 0, waits indefinitely.
|
||||||
|
//
|
||||||
|
// Default: 500
|
||||||
|
"lsp_fetch_timeout_ms": 500
|
||||||
|
},
|
||||||
// Different settings for specific languages.
|
// Different settings for specific languages.
|
||||||
"languages": {
|
"languages": {
|
||||||
"Astro": {
|
"Astro": {
|
||||||
|
|
|
@ -271,7 +271,10 @@ mod tests {
|
||||||
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal};
|
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
language_settings::{
|
||||||
|
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||||
|
WordsCompletionMode,
|
||||||
|
},
|
||||||
Point,
|
Point,
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
@ -286,7 +289,13 @@ mod tests {
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||||
// flaky
|
// flaky
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |settings| {
|
||||||
|
settings.defaults.completions = Some(CompletionSettings {
|
||||||
|
words: WordsCompletionMode::Disabled,
|
||||||
|
lsp: true,
|
||||||
|
lsp_fetch_timeout_ms: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||||
let mut cx = EditorLspTestContext::new_rust(
|
let mut cx = EditorLspTestContext::new_rust(
|
||||||
|
@ -511,7 +520,13 @@ mod tests {
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) {
|
) {
|
||||||
// flaky
|
// flaky
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |settings| {
|
||||||
|
settings.defaults.completions = Some(CompletionSettings {
|
||||||
|
words: WordsCompletionMode::Disabled,
|
||||||
|
lsp: true,
|
||||||
|
lsp_fetch_timeout_ms: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||||
let mut cx = EditorLspTestContext::new_rust(
|
let mut cx = EditorLspTestContext::new_rust(
|
||||||
|
|
|
@ -101,6 +101,7 @@ use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{
|
language_settings::{
|
||||||
self, all_language_settings, language_settings, InlayHintSettings, RewrapBehavior,
|
self, all_language_settings, language_settings, InlayHintSettings, RewrapBehavior,
|
||||||
|
WordsCompletionMode,
|
||||||
},
|
},
|
||||||
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
|
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
|
||||||
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, EditPredictionsMode,
|
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, EditPredictionsMode,
|
||||||
|
@ -4012,9 +4013,8 @@ impl Editor {
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let show_completion_documentation = buffer
|
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||||
.read(cx)
|
let show_completion_documentation = buffer_snapshot
|
||||||
.snapshot()
|
|
||||||
.settings_at(buffer_position, cx)
|
.settings_at(buffer_position, cx)
|
||||||
.show_completion_documentation;
|
.show_completion_documentation;
|
||||||
|
|
||||||
|
@ -4038,6 +4038,51 @@ impl Editor {
|
||||||
};
|
};
|
||||||
let completions =
|
let completions =
|
||||||
provider.completions(&buffer, buffer_position, completion_context, window, cx);
|
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 sort_completions = provider.sort_completions();
|
||||||
|
|
||||||
let id = post_inc(&mut self.next_completion_id);
|
let id = post_inc(&mut self.next_completion_id);
|
||||||
|
@ -4046,8 +4091,55 @@ impl Editor {
|
||||||
editor.update(&mut cx, |this, _| {
|
editor.update(&mut cx, |this, _| {
|
||||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||||
})?;
|
})?;
|
||||||
let completions = completions.await.log_err();
|
let mut completions = completions.await.log_err().unwrap_or_default();
|
||||||
let menu = if let Some(completions) = completions {
|
|
||||||
|
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(
|
let mut menu = CompletionsMenu::new(
|
||||||
id,
|
id,
|
||||||
sort_completions,
|
sort_completions,
|
||||||
|
@ -4061,8 +4153,6 @@ impl Editor {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
menu.visible().then_some(menu)
|
menu.visible().then_some(menu)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
|
||||||
editor.update_in(&mut cx, |editor, window, cx| {
|
editor.update_in(&mut cx, |editor, window, cx| {
|
||||||
|
|
|
@ -16,7 +16,8 @@ use gpui::{
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{
|
language_settings::{
|
||||||
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
|
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
|
||||||
|
LanguageSettingsContent, PrettierSettings,
|
||||||
},
|
},
|
||||||
BracketPairConfig,
|
BracketPairConfig,
|
||||||
Capability::ReadWrite,
|
Capability::ReadWrite,
|
||||||
|
@ -30,7 +31,7 @@ use pretty_assertions::{assert_eq, assert_ne};
|
||||||
use project::project_settings::{LspSettings, ProjectSettings};
|
use project::project_settings::{LspSettings, ProjectSettings};
|
||||||
use project::FakeFs;
|
use project::FakeFs;
|
||||||
use serde_json::{self, json};
|
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::{
|
use std::{
|
||||||
iter,
|
iter,
|
||||||
sync::atomic::{self, AtomicUsize},
|
sync::atomic::{self, AtomicUsize},
|
||||||
|
@ -9169,6 +9170,101 @@ async fn test_completion(cx: &mut TestAppContext) {
|
||||||
apply_additional_edits.await.unwrap();
|
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]
|
#[gpui::test]
|
||||||
async fn test_multiline_completion(cx: &mut TestAppContext) {
|
async fn test_multiline_completion(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
|
@ -4145,6 +4145,63 @@ impl BufferSnapshot {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn words_in_range(
|
||||||
|
&self,
|
||||||
|
query: Option<&str>,
|
||||||
|
range: Range<usize>,
|
||||||
|
) -> HashMap<String, Range<Anchor>> {
|
||||||
|
if query.map_or(false, |query| query.is_empty()) {
|
||||||
|
return HashMap::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let classifier = CharClassifier::new(self.language.clone().map(|language| LanguageScope {
|
||||||
|
language,
|
||||||
|
override_id: None,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mut query_ix = 0;
|
||||||
|
let query = query.map(|query| query.chars().collect::<Vec<_>>());
|
||||||
|
let query_len = query.as_ref().map_or(0, |query| query.len());
|
||||||
|
|
||||||
|
let mut words = HashMap::default();
|
||||||
|
let mut current_word_start_ix = None;
|
||||||
|
let mut chunk_ix = range.start;
|
||||||
|
for chunk in self.chunks(range, false) {
|
||||||
|
for (i, c) in chunk.text.char_indices() {
|
||||||
|
let ix = chunk_ix + i;
|
||||||
|
if classifier.is_word(c) {
|
||||||
|
if current_word_start_ix.is_none() {
|
||||||
|
current_word_start_ix = Some(ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(query) = &query {
|
||||||
|
if query_ix < query_len {
|
||||||
|
let query_c = query.get(query_ix).expect(
|
||||||
|
"query_ix is a vec of chars, which we access only if before the end",
|
||||||
|
);
|
||||||
|
if c.to_lowercase().eq(query_c.to_lowercase()) {
|
||||||
|
query_ix += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} else if let Some(word_start) = current_word_start_ix.take() {
|
||||||
|
if query_ix == query_len {
|
||||||
|
let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
|
||||||
|
words.insert(
|
||||||
|
self.text_for_range(word_start..ix).collect::<String>(),
|
||||||
|
word_range,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query_ix = 0;
|
||||||
|
}
|
||||||
|
chunk_ix += chunk.text.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
words
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
|
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
|
||||||
|
|
|
@ -13,6 +13,7 @@ use proto::deserialize_operation;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use regex::RegexBuilder;
|
use regex::RegexBuilder;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
|
@ -3140,6 +3141,93 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_words_in_range(cx: &mut gpui::App) {
|
||||||
|
init_settings(cx, |_| {});
|
||||||
|
|
||||||
|
let contents = r#"let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word"#;
|
||||||
|
|
||||||
|
let buffer = cx.new(|cx| {
|
||||||
|
let buffer = Buffer::local(contents, cx).with_language(Arc::new(rust_lang()), cx);
|
||||||
|
assert_eq!(buffer.text(), contents);
|
||||||
|
buffer.check_invariants();
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
buffer.update(cx, |buffer, _| {
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
assert_eq!(
|
||||||
|
BTreeSet::from_iter(["Pizza".to_string()]),
|
||||||
|
snapshot
|
||||||
|
.words_in_range(Some("piz"), 0..snapshot.len())
|
||||||
|
.into_keys()
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BTreeSet::from_iter([
|
||||||
|
"öäpple".to_string(),
|
||||||
|
"Öäpple".to_string(),
|
||||||
|
"öÄpPlE".to_string(),
|
||||||
|
"ÖÄPPLE".to_string(),
|
||||||
|
]),
|
||||||
|
snapshot
|
||||||
|
.words_in_range(Some("öp"), 0..snapshot.len())
|
||||||
|
.into_keys()
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BTreeSet::from_iter([
|
||||||
|
"öÄpPlE".to_string(),
|
||||||
|
"Öäpple".to_string(),
|
||||||
|
"ÖÄPPLE".to_string(),
|
||||||
|
"öäpple".to_string(),
|
||||||
|
]),
|
||||||
|
snapshot
|
||||||
|
.words_in_range(Some("öÄ"), 0..snapshot.len())
|
||||||
|
.into_keys()
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BTreeSet::default(),
|
||||||
|
snapshot
|
||||||
|
.words_in_range(Some("öÄ好"), 0..snapshot.len())
|
||||||
|
.into_keys()
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BTreeSet::from_iter(["bar你".to_string(),]),
|
||||||
|
snapshot
|
||||||
|
.words_in_range(Some("你"), 0..snapshot.len())
|
||||||
|
.into_keys()
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BTreeSet::default(),
|
||||||
|
snapshot
|
||||||
|
.words_in_range(Some(""), 0..snapshot.len())
|
||||||
|
.into_keys()
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BTreeSet::from_iter([
|
||||||
|
"bar你".to_string(),
|
||||||
|
"öÄpPlE".to_string(),
|
||||||
|
"Öäpple".to_string(),
|
||||||
|
"ÖÄPPLE".to_string(),
|
||||||
|
"öäpple".to_string(),
|
||||||
|
"let".to_string(),
|
||||||
|
"Pizza".to_string(),
|
||||||
|
"word".to_string(),
|
||||||
|
"word2".to_string(),
|
||||||
|
]),
|
||||||
|
snapshot
|
||||||
|
.words_in_range(None, 0..snapshot.len())
|
||||||
|
.into_keys()
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn ruby_lang() -> Language {
|
fn ruby_lang() -> Language {
|
||||||
Language::new(
|
Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
|
|
|
@ -79,10 +79,10 @@ pub struct LanguageSettings {
|
||||||
/// The column at which to soft-wrap lines, for buffers where soft-wrap
|
/// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||||
/// is enabled.
|
/// is enabled.
|
||||||
pub preferred_line_length: u32,
|
pub preferred_line_length: u32,
|
||||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
/// Whether to show wrap guides (vertical rulers) in the editor.
|
||||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
/// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||||
// if softwrap is set to 'preferred_line_length', and will show any
|
/// if softwrap is set to 'preferred_line_length', and will show any
|
||||||
// additional guides as specified by the 'wrap_guides' setting.
|
/// additional guides as specified by the 'wrap_guides' setting.
|
||||||
pub show_wrap_guides: bool,
|
pub show_wrap_guides: bool,
|
||||||
/// Character counts at which to show wrap guides (vertical rulers) in the editor.
|
/// Character counts at which to show wrap guides (vertical rulers) in the editor.
|
||||||
pub wrap_guides: Vec<usize>,
|
pub wrap_guides: Vec<usize>,
|
||||||
|
@ -137,7 +137,7 @@ pub struct LanguageSettings {
|
||||||
pub use_on_type_format: bool,
|
pub use_on_type_format: bool,
|
||||||
/// Whether indentation of pasted content should be adjusted based on the context.
|
/// Whether indentation of pasted content should be adjusted based on the context.
|
||||||
pub auto_indent_on_paste: bool,
|
pub auto_indent_on_paste: bool,
|
||||||
// Controls how the editor handles the autoclosed characters.
|
/// Controls how the editor handles the autoclosed characters.
|
||||||
pub always_treat_brackets_as_autoclosed: bool,
|
pub always_treat_brackets_as_autoclosed: bool,
|
||||||
/// Which code actions to run on save
|
/// Which code actions to run on save
|
||||||
pub code_actions_on_format: HashMap<String, bool>,
|
pub code_actions_on_format: HashMap<String, bool>,
|
||||||
|
@ -151,6 +151,8 @@ pub struct LanguageSettings {
|
||||||
/// Whether to display inline and alongside documentation for items in the
|
/// Whether to display inline and alongside documentation for items in the
|
||||||
/// completions menu.
|
/// completions menu.
|
||||||
pub show_completion_documentation: bool,
|
pub show_completion_documentation: bool,
|
||||||
|
/// Completion settings for this language.
|
||||||
|
pub completions: CompletionSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageSettings {
|
impl LanguageSettings {
|
||||||
|
@ -306,6 +308,50 @@ pub struct AllLanguageSettingsContent {
|
||||||
pub file_types: HashMap<Arc<str>, Vec<String>>,
|
pub file_types: HashMap<Arc<str>, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controls how completions are processed for this language.
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub struct CompletionSettings {
|
||||||
|
/// Controls how words are completed.
|
||||||
|
/// For large documents, not all words may be fetched for completion.
|
||||||
|
///
|
||||||
|
/// Default: `fallback`
|
||||||
|
#[serde(default = "default_words_completion_mode")]
|
||||||
|
pub words: WordsCompletionMode,
|
||||||
|
/// Whether to fetch LSP completions or not.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub lsp: bool,
|
||||||
|
/// When fetching LSP completions, determines how long to wait for a response of a particular server.
|
||||||
|
/// When set to 0, waits indefinitely.
|
||||||
|
///
|
||||||
|
/// Default: 500
|
||||||
|
#[serde(default = "lsp_fetch_timeout_ms")]
|
||||||
|
pub lsp_fetch_timeout_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controls how document's words are completed.
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum WordsCompletionMode {
|
||||||
|
/// Always fetch document's words for completions.
|
||||||
|
Enabled,
|
||||||
|
/// Only if LSP response errors/times out/is empty,
|
||||||
|
/// use document's words to show completions.
|
||||||
|
Fallback,
|
||||||
|
/// Never fetch or complete document's words for completions.
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_words_completion_mode() -> WordsCompletionMode {
|
||||||
|
WordsCompletionMode::Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lsp_fetch_timeout_ms() -> u64 {
|
||||||
|
500
|
||||||
|
}
|
||||||
|
|
||||||
/// The settings for a particular language.
|
/// The settings for a particular language.
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct LanguageSettingsContent {
|
pub struct LanguageSettingsContent {
|
||||||
|
@ -478,6 +524,8 @@ pub struct LanguageSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: true
|
/// Default: true
|
||||||
pub show_completion_documentation: Option<bool>,
|
pub show_completion_documentation: Option<bool>,
|
||||||
|
/// Controls how completions are processed for this language.
|
||||||
|
pub completions: Option<CompletionSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The behavior of `editor::Rewrap`.
|
/// The behavior of `editor::Rewrap`.
|
||||||
|
@ -1381,6 +1429,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||||
&mut settings.show_completion_documentation,
|
&mut settings.show_completion_documentation,
|
||||||
src.show_completion_documentation,
|
src.show_completion_documentation,
|
||||||
);
|
);
|
||||||
|
merge(&mut settings.completions, src.completions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allows to enable/disable formatting with Prettier
|
/// Allows to enable/disable formatting with Prettier
|
||||||
|
|
|
@ -23,13 +23,13 @@ use client::{proto, TypedEnvelope};
|
||||||
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||||
use futures::{
|
use futures::{
|
||||||
future::{join_all, Shared},
|
future::{join_all, Shared},
|
||||||
select,
|
select, select_biased,
|
||||||
stream::FuturesUnordered,
|
stream::FuturesUnordered,
|
||||||
AsyncWriteExt, Future, FutureExt, StreamExt,
|
AsyncWriteExt, Future, FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
|
App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
|
||||||
WeakEntity,
|
WeakEntity,
|
||||||
};
|
};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
|
@ -4325,6 +4325,15 @@ impl LspStore {
|
||||||
let offset = position.to_offset(&snapshot);
|
let offset = position.to_offset(&snapshot);
|
||||||
let scope = snapshot.language_scope_at(offset);
|
let scope = snapshot.language_scope_at(offset);
|
||||||
let language = snapshot.language().cloned();
|
let language = snapshot.language().cloned();
|
||||||
|
let completion_settings = language_settings(
|
||||||
|
language.as_ref().map(|language| language.name()),
|
||||||
|
buffer.read(cx).file(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.completions;
|
||||||
|
if !completion_settings.lsp {
|
||||||
|
return Task::ready(Ok(Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
|
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
|
||||||
local
|
local
|
||||||
|
@ -4341,23 +4350,51 @@ impl LspStore {
|
||||||
});
|
});
|
||||||
|
|
||||||
let buffer = buffer.clone();
|
let buffer = buffer.clone();
|
||||||
|
let lsp_timeout = completion_settings.lsp_fetch_timeout_ms;
|
||||||
|
let lsp_timeout = if lsp_timeout > 0 {
|
||||||
|
Some(Duration::from_millis(lsp_timeout))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let mut tasks = Vec::with_capacity(server_ids.len());
|
let mut tasks = Vec::with_capacity(server_ids.len());
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |lsp_store, cx| {
|
||||||
for server_id in server_ids {
|
for server_id in server_ids {
|
||||||
let lsp_adapter = this.language_server_adapter_for_id(server_id);
|
let lsp_adapter = lsp_store.language_server_adapter_for_id(server_id);
|
||||||
tasks.push((
|
let lsp_timeout = lsp_timeout
|
||||||
lsp_adapter,
|
.map(|lsp_timeout| cx.background_executor().timer(lsp_timeout));
|
||||||
this.request_lsp(
|
let mut timeout = cx.background_spawn(async move {
|
||||||
buffer.clone(),
|
match lsp_timeout {
|
||||||
LanguageServerToQuery::Other(server_id),
|
Some(lsp_timeout) => {
|
||||||
GetCompletions {
|
lsp_timeout.await;
|
||||||
position,
|
true
|
||||||
context: context.clone(),
|
|
||||||
},
|
},
|
||||||
cx,
|
None => false,
|
||||||
),
|
}
|
||||||
));
|
}).fuse();
|
||||||
|
let mut lsp_request = lsp_store.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Other(server_id),
|
||||||
|
GetCompletions {
|
||||||
|
position,
|
||||||
|
context: context.clone(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
).fuse();
|
||||||
|
let new_task = cx.background_spawn(async move {
|
||||||
|
select_biased! {
|
||||||
|
response = lsp_request => response,
|
||||||
|
timeout_happened = timeout => {
|
||||||
|
if timeout_happened {
|
||||||
|
log::warn!("Fetching completions from server {server_id} timed out, timeout ms: {}", completion_settings.lsp_fetch_timeout_ms);
|
||||||
|
return anyhow::Ok(Vec::new())
|
||||||
|
} else {
|
||||||
|
lsp_request.await
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tasks.push((lsp_adapter, new_task));
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -4416,47 +4453,58 @@ impl LspStore {
|
||||||
{
|
{
|
||||||
did_resolve = true;
|
did_resolve = true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
resolve_word_completion(
|
||||||
|
&buffer_snapshot,
|
||||||
|
&mut completions.borrow_mut()[completion_index],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for completion_index in completion_indices {
|
for completion_index in completion_indices {
|
||||||
let Some(server_id) = completions.borrow()[completion_index].source.server_id()
|
let server_id = {
|
||||||
else {
|
let completion = &completions.borrow()[completion_index];
|
||||||
continue;
|
completion.source.server_id()
|
||||||
};
|
};
|
||||||
|
if let Some(server_id) = server_id {
|
||||||
|
let server_and_adapter = this
|
||||||
|
.read_with(&cx, |lsp_store, _| {
|
||||||
|
let server = lsp_store.language_server_for_id(server_id)?;
|
||||||
|
let adapter =
|
||||||
|
lsp_store.language_server_adapter_for_id(server.server_id())?;
|
||||||
|
Some((server, adapter))
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
let Some((server, adapter)) = server_and_adapter else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let server_and_adapter = this
|
let resolved = Self::resolve_completion_local(
|
||||||
.read_with(&cx, |lsp_store, _| {
|
server,
|
||||||
let server = lsp_store.language_server_for_id(server_id)?;
|
|
||||||
let adapter =
|
|
||||||
lsp_store.language_server_adapter_for_id(server.server_id())?;
|
|
||||||
Some((server, adapter))
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
.flatten();
|
|
||||||
let Some((server, adapter)) = server_and_adapter else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let resolved = Self::resolve_completion_local(
|
|
||||||
server,
|
|
||||||
&buffer_snapshot,
|
|
||||||
completions.clone(),
|
|
||||||
completion_index,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.log_err()
|
|
||||||
.is_some();
|
|
||||||
if resolved {
|
|
||||||
Self::regenerate_completion_labels(
|
|
||||||
adapter,
|
|
||||||
&buffer_snapshot,
|
&buffer_snapshot,
|
||||||
completions.clone(),
|
completions.clone(),
|
||||||
completion_index,
|
completion_index,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.log_err();
|
.log_err()
|
||||||
did_resolve = true;
|
.is_some();
|
||||||
|
if resolved {
|
||||||
|
Self::regenerate_completion_labels(
|
||||||
|
adapter,
|
||||||
|
&buffer_snapshot,
|
||||||
|
completions.clone(),
|
||||||
|
completion_index,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
did_resolve = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve_word_completion(
|
||||||
|
&buffer_snapshot,
|
||||||
|
&mut completions.borrow_mut()[completion_index],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4500,7 +4548,9 @@ impl LspStore {
|
||||||
);
|
);
|
||||||
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
|
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
|
||||||
}
|
}
|
||||||
CompletionSource::Custom => return Ok(()),
|
CompletionSource::BufferWord { .. } | CompletionSource::Custom => {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let resolved_completion = request.await?;
|
let resolved_completion = request.await?;
|
||||||
|
@ -4641,7 +4691,9 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
serde_json::to_string(lsp_completion).unwrap().into_bytes()
|
serde_json::to_string(lsp_completion).unwrap().into_bytes()
|
||||||
}
|
}
|
||||||
CompletionSource::Custom => return Ok(()),
|
CompletionSource::Custom | CompletionSource::BufferWord { .. } => {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let request = proto::ResolveCompletionDocumentation {
|
let request = proto::ResolveCompletionDocumentation {
|
||||||
|
@ -8172,51 +8224,54 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
|
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
|
||||||
let (source, server_id, lsp_completion, lsp_defaults, resolved) = match &completion.source {
|
let mut serialized_completion = proto::Completion {
|
||||||
|
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
||||||
|
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
||||||
|
new_text: completion.new_text.clone(),
|
||||||
|
..proto::Completion::default()
|
||||||
|
};
|
||||||
|
match &completion.source {
|
||||||
CompletionSource::Lsp {
|
CompletionSource::Lsp {
|
||||||
server_id,
|
server_id,
|
||||||
lsp_completion,
|
lsp_completion,
|
||||||
lsp_defaults,
|
lsp_defaults,
|
||||||
resolved,
|
resolved,
|
||||||
} => (
|
} => {
|
||||||
proto::completion::Source::Lsp as i32,
|
serialized_completion.source = proto::completion::Source::Lsp as i32;
|
||||||
server_id.0 as u64,
|
serialized_completion.server_id = server_id.0 as u64;
|
||||||
serde_json::to_vec(lsp_completion).unwrap(),
|
serialized_completion.lsp_completion = serde_json::to_vec(lsp_completion).unwrap();
|
||||||
lsp_defaults
|
serialized_completion.lsp_defaults = lsp_defaults
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap()),
|
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap());
|
||||||
*resolved,
|
serialized_completion.resolved = *resolved;
|
||||||
),
|
}
|
||||||
CompletionSource::Custom => (
|
CompletionSource::BufferWord {
|
||||||
proto::completion::Source::Custom as i32,
|
word_range,
|
||||||
0,
|
resolved,
|
||||||
Vec::new(),
|
} => {
|
||||||
None,
|
serialized_completion.source = proto::completion::Source::BufferWord as i32;
|
||||||
true,
|
serialized_completion.buffer_word_start = Some(serialize_anchor(&word_range.start));
|
||||||
),
|
serialized_completion.buffer_word_end = Some(serialize_anchor(&word_range.end));
|
||||||
};
|
serialized_completion.resolved = *resolved;
|
||||||
|
}
|
||||||
proto::Completion {
|
CompletionSource::Custom => {
|
||||||
old_start: Some(serialize_anchor(&completion.old_range.start)),
|
serialized_completion.source = proto::completion::Source::Custom as i32;
|
||||||
old_end: Some(serialize_anchor(&completion.old_range.end)),
|
serialized_completion.resolved = true;
|
||||||
new_text: completion.new_text.clone(),
|
}
|
||||||
server_id,
|
|
||||||
lsp_completion,
|
|
||||||
lsp_defaults,
|
|
||||||
resolved,
|
|
||||||
source,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serialized_completion
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result<CoreCompletion> {
|
pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result<CoreCompletion> {
|
||||||
let old_start = completion
|
let old_start = completion
|
||||||
.old_start
|
.old_start
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("invalid old start"))?;
|
.context("invalid old start")?;
|
||||||
let old_end = completion
|
let old_end = completion
|
||||||
.old_end
|
.old_end
|
||||||
.and_then(deserialize_anchor)
|
.and_then(deserialize_anchor)
|
||||||
.ok_or_else(|| anyhow!("invalid old end"))?;
|
.context("invalid old end")?;
|
||||||
Ok(CoreCompletion {
|
Ok(CoreCompletion {
|
||||||
old_range: old_start..old_end,
|
old_range: old_start..old_end,
|
||||||
new_text: completion.new_text,
|
new_text: completion.new_text,
|
||||||
|
@ -8232,6 +8287,20 @@ impl LspStore {
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
resolved: completion.resolved,
|
resolved: completion.resolved,
|
||||||
},
|
},
|
||||||
|
Some(proto::completion::Source::BufferWord) => {
|
||||||
|
let word_range = completion
|
||||||
|
.buffer_word_start
|
||||||
|
.and_then(deserialize_anchor)
|
||||||
|
.context("invalid buffer word start")?
|
||||||
|
..completion
|
||||||
|
.buffer_word_end
|
||||||
|
.and_then(deserialize_anchor)
|
||||||
|
.context("invalid buffer word end")?;
|
||||||
|
CompletionSource::BufferWord {
|
||||||
|
word_range,
|
||||||
|
resolved: completion.resolved,
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
|
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -8296,6 +8365,40 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) {
|
||||||
|
let CompletionSource::BufferWord {
|
||||||
|
word_range,
|
||||||
|
resolved,
|
||||||
|
} = &mut completion.source
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if *resolved {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if completion.new_text
|
||||||
|
!= snapshot
|
||||||
|
.text_for_range(word_range.clone())
|
||||||
|
.collect::<String>()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut offset = 0;
|
||||||
|
for chunk in snapshot.chunks(word_range.clone(), true) {
|
||||||
|
let end_offset = offset + chunk.text.len();
|
||||||
|
if let Some(highlight_id) = chunk.syntax_highlight_id {
|
||||||
|
completion
|
||||||
|
.label
|
||||||
|
.runs
|
||||||
|
.push((offset..end_offset, highlight_id));
|
||||||
|
}
|
||||||
|
offset = end_offset;
|
||||||
|
}
|
||||||
|
*resolved = true;
|
||||||
|
}
|
||||||
|
|
||||||
impl EventEmitter<LspStoreEvent> for LspStore {}
|
impl EventEmitter<LspStoreEvent> for LspStore {}
|
||||||
|
|
||||||
fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
|
fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
|
||||||
|
|
|
@ -388,6 +388,10 @@ pub enum CompletionSource {
|
||||||
resolved: bool,
|
resolved: bool,
|
||||||
},
|
},
|
||||||
Custom,
|
Custom,
|
||||||
|
BufferWord {
|
||||||
|
word_range: Range<Anchor>,
|
||||||
|
resolved: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompletionSource {
|
impl CompletionSource {
|
||||||
|
|
|
@ -1002,10 +1002,13 @@ message Completion {
|
||||||
bool resolved = 6;
|
bool resolved = 6;
|
||||||
Source source = 7;
|
Source source = 7;
|
||||||
optional bytes lsp_defaults = 8;
|
optional bytes lsp_defaults = 8;
|
||||||
|
optional Anchor buffer_word_start = 9;
|
||||||
|
optional Anchor buffer_word_end = 10;
|
||||||
|
|
||||||
enum Source {
|
enum Source {
|
||||||
Lsp = 0;
|
Lsp = 0;
|
||||||
Custom = 1;
|
Custom = 1;
|
||||||
|
BufferWord = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue