Avoid re-querying language server completions when possible (#31872)
Also adds reuse of the markdown documentation cache even when completions are re-queried, so that markdown documentation doesn't flicker when `is_incomplete: true` (completions provided by rust analyzer always set this) Release Notes: - Added support for filtering language server completions instead of re-querying.
This commit is contained in:
parent
b7ec437b13
commit
17cf865d1e
17 changed files with 1221 additions and 720 deletions
|
@ -14,7 +14,7 @@ use http_client::HttpClientWithUrl;
|
|||
use itertools::Itertools;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
|
||||
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use text::{Anchor, OffsetRangeExt, ToPoint};
|
||||
|
@ -746,7 +746,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
|||
_trigger: CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let state = buffer.update(cx, |buffer, _cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
|
@ -756,13 +756,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
|||
MentionCompletion::try_parse(line, offset_to_line)
|
||||
});
|
||||
let Some(state) = state else {
|
||||
return Task::ready(Ok(None));
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let Some((workspace, context_store)) =
|
||||
self.workspace.upgrade().zip(self.context_store.upgrade())
|
||||
else {
|
||||
return Task::ready(Ok(None));
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
@ -815,10 +815,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
|||
cx.spawn(async move |_, cx| {
|
||||
let matches = search_task.await;
|
||||
let Some(editor) = editor.upgrade() else {
|
||||
return Ok(None);
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
Ok(Some(cx.update(|cx| {
|
||||
let completions = cx.update(|cx| {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| match mat {
|
||||
|
@ -901,7 +901,14 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
|||
),
|
||||
})
|
||||
.collect()
|
||||
})?))
|
||||
})?;
|
||||
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
// Since this does its own filtering (see `filter_completions()` returns false),
|
||||
// there is no benefit to computing whether this set of completions is incomplete.
|
||||
is_incomplete: true,
|
||||
}])
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ impl SlashCommandCompletionProvider {
|
|||
name_range: Range<Anchor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
||||
) -> Task<Result<Vec<project::CompletionResponse>>> {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let candidates = slash_commands
|
||||
.command_names(cx)
|
||||
|
@ -71,28 +71,27 @@ impl SlashCommandCompletionProvider {
|
|||
.await;
|
||||
|
||||
cx.update(|_, cx| {
|
||||
Some(
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| {
|
||||
let command = slash_commands.command(&mat.string, cx)?;
|
||||
let mut new_text = mat.string.clone();
|
||||
let requires_argument = command.requires_argument();
|
||||
let accepts_arguments = command.accepts_arguments();
|
||||
if requires_argument || accepts_arguments {
|
||||
new_text.push(' ');
|
||||
}
|
||||
let completions = matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| {
|
||||
let command = slash_commands.command(&mat.string, cx)?;
|
||||
let mut new_text = mat.string.clone();
|
||||
let requires_argument = command.requires_argument();
|
||||
let accepts_arguments = command.accepts_arguments();
|
||||
if requires_argument || accepts_arguments {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(
|
||||
move |intent: CompletionIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut App| {
|
||||
|
@ -118,22 +117,27 @@ impl SlashCommandCompletionProvider {
|
|||
}
|
||||
},
|
||||
) as Arc<_>
|
||||
});
|
||||
Some(project::Completion {
|
||||
replace_range: name_range.clone(),
|
||||
documentation: Some(CompletionDocumentation::SingleLine(
|
||||
command.description().into(),
|
||||
)),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
icon_path: None,
|
||||
insert_text_mode: None,
|
||||
confirm,
|
||||
source: CompletionSource::Custom,
|
||||
})
|
||||
});
|
||||
|
||||
Some(project::Completion {
|
||||
replace_range: name_range.clone(),
|
||||
documentation: Some(CompletionDocumentation::SingleLine(
|
||||
command.description().into(),
|
||||
)),
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
icon_path: None,
|
||||
insert_text_mode: None,
|
||||
confirm,
|
||||
source: CompletionSource::Custom,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
vec![project::CompletionResponse {
|
||||
completions,
|
||||
is_incomplete: false,
|
||||
}]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -147,7 +151,7 @@ impl SlashCommandCompletionProvider {
|
|||
last_argument_range: Range<Anchor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
||||
) -> Task<Result<Vec<project::CompletionResponse>>> {
|
||||
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let mut flag = self.cancel_flag.lock();
|
||||
flag.store(true, SeqCst);
|
||||
|
@ -165,28 +169,27 @@ impl SlashCommandCompletionProvider {
|
|||
let workspace = self.workspace.clone();
|
||||
let arguments = arguments.to_vec();
|
||||
cx.background_spawn(async move {
|
||||
Ok(Some(
|
||||
completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|new_argument| {
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
Arc::new({
|
||||
let mut completed_arguments = arguments.clone();
|
||||
if new_argument.replace_previous_arguments {
|
||||
completed_arguments.clear();
|
||||
} else {
|
||||
completed_arguments.pop();
|
||||
}
|
||||
completed_arguments.push(new_argument.new_text.clone());
|
||||
let completions = completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|new_argument| {
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
Arc::new({
|
||||
let mut completed_arguments = arguments.clone();
|
||||
if new_argument.replace_previous_arguments {
|
||||
completed_arguments.clear();
|
||||
} else {
|
||||
completed_arguments.pop();
|
||||
}
|
||||
completed_arguments.push(new_argument.new_text.clone());
|
||||
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
move |intent: CompletionIntent,
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
move |intent: CompletionIntent,
|
||||
window: &mut Window,
|
||||
cx: &mut App| {
|
||||
if new_argument.after_completion.run()
|
||||
|
@ -210,34 +213,41 @@ impl SlashCommandCompletionProvider {
|
|||
!new_argument.after_completion.run()
|
||||
}
|
||||
}
|
||||
}) as Arc<_>
|
||||
});
|
||||
}) as Arc<_>
|
||||
});
|
||||
|
||||
let mut new_text = new_argument.new_text.clone();
|
||||
if new_argument.after_completion == AfterCompletion::Continue {
|
||||
new_text.push(' ');
|
||||
}
|
||||
let mut new_text = new_argument.new_text.clone();
|
||||
if new_argument.after_completion == AfterCompletion::Continue {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
project::Completion {
|
||||
replace_range: if new_argument.replace_previous_arguments {
|
||||
argument_range.clone()
|
||||
} else {
|
||||
last_argument_range.clone()
|
||||
},
|
||||
label: new_argument.label,
|
||||
icon_path: None,
|
||||
new_text,
|
||||
documentation: None,
|
||||
confirm,
|
||||
insert_text_mode: None,
|
||||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
project::Completion {
|
||||
replace_range: if new_argument.replace_previous_arguments {
|
||||
argument_range.clone()
|
||||
} else {
|
||||
last_argument_range.clone()
|
||||
},
|
||||
label: new_argument.label,
|
||||
icon_path: None,
|
||||
new_text,
|
||||
documentation: None,
|
||||
confirm,
|
||||
insert_text_mode: None,
|
||||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(vec![project::CompletionResponse {
|
||||
completions,
|
||||
is_incomplete: false,
|
||||
}])
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(Some(Vec::new())))
|
||||
Task::ready(Ok(vec![project::CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
is_incomplete: false,
|
||||
}]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -251,7 +261,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
|||
_: editor::CompletionContext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
||||
) -> Task<Result<Vec<project::CompletionResponse>>> {
|
||||
let Some((name, arguments, command_range, last_argument_range)) =
|
||||
buffer.update(cx, |buffer, _cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
|
@ -295,7 +305,10 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
|||
Some((name, arguments, command_range, last_argument_range))
|
||||
})
|
||||
else {
|
||||
return Task::ready(Ok(Some(Vec::new())));
|
||||
return Task::ready(Ok(vec![project::CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
is_incomplete: false,
|
||||
}]));
|
||||
};
|
||||
|
||||
if let Some((arguments, argument_range)) = arguments {
|
||||
|
|
|
@ -12,7 +12,7 @@ use language::{
|
|||
Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, ToOffset,
|
||||
language_settings::SoftWrap,
|
||||
};
|
||||
use project::{Completion, CompletionSource, search::SearchQuery};
|
||||
use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
|
@ -64,9 +64,9 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
|||
_: editor::CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let Some(handle) = self.0.upgrade() else {
|
||||
return Task::ready(Ok(None));
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
handle.update(cx, |message_editor, cx| {
|
||||
message_editor.completions(buffer, buffer_position, cx)
|
||||
|
@ -248,22 +248,21 @@ impl MessageEditor {
|
|||
buffer: &Entity<Buffer>,
|
||||
end_anchor: Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
if let Some((start_anchor, query, candidates)) =
|
||||
self.collect_mention_candidates(buffer, end_anchor, cx)
|
||||
{
|
||||
if !candidates.is_empty() {
|
||||
return cx.spawn(async move |_, cx| {
|
||||
Ok(Some(
|
||||
Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
&candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_mention,
|
||||
)
|
||||
.await,
|
||||
))
|
||||
let completion_response = Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
&candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_mention,
|
||||
)
|
||||
.await;
|
||||
Ok(vec![completion_response])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -273,21 +272,23 @@ impl MessageEditor {
|
|||
{
|
||||
if !candidates.is_empty() {
|
||||
return cx.spawn(async move |_, cx| {
|
||||
Ok(Some(
|
||||
Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_emoji,
|
||||
)
|
||||
.await,
|
||||
))
|
||||
let completion_response = Self::resolve_completions_for_candidates(
|
||||
&cx,
|
||||
query.as_str(),
|
||||
candidates,
|
||||
start_anchor..end_anchor,
|
||||
Self::completion_for_emoji,
|
||||
)
|
||||
.await;
|
||||
Ok(vec![completion_response])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Task::ready(Ok(Some(Vec::new())))
|
||||
Task::ready(Ok(vec![CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
is_incomplete: false,
|
||||
}]))
|
||||
}
|
||||
|
||||
async fn resolve_completions_for_candidates(
|
||||
|
@ -296,18 +297,19 @@ impl MessageEditor {
|
|||
candidates: &[StringMatchCandidate],
|
||||
range: Range<Anchor>,
|
||||
completion_fn: impl Fn(&StringMatch) -> (String, CodeLabel),
|
||||
) -> Vec<Completion> {
|
||||
) -> CompletionResponse {
|
||||
const LIMIT: usize = 10;
|
||||
let matches = fuzzy::match_strings(
|
||||
candidates,
|
||||
query,
|
||||
true,
|
||||
10,
|
||||
LIMIT,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
let completions = matches
|
||||
.into_iter()
|
||||
.map(|mat| {
|
||||
let (new_text, label) = completion_fn(&mat);
|
||||
|
@ -322,7 +324,12 @@ impl MessageEditor {
|
|||
source: CompletionSource::Custom,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
CompletionResponse {
|
||||
is_incomplete: completions.len() >= LIMIT,
|
||||
completions,
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_for_mention(mat: &StringMatch) -> (String, CodeLabel) {
|
||||
|
|
|
@ -13,7 +13,7 @@ use gpui::{
|
|||
use language::{Buffer, CodeLabel, ToOffset};
|
||||
use menu::Confirm;
|
||||
use project::{
|
||||
Completion,
|
||||
Completion, CompletionResponse,
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
|
||||
};
|
||||
use settings::Settings;
|
||||
|
@ -262,9 +262,9 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
|||
_trigger: editor::CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let Some(console) = self.0.upgrade() else {
|
||||
return Task::ready(Ok(None));
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let support_completions = console
|
||||
|
@ -322,7 +322,7 @@ impl ConsoleQueryBarCompletionProvider {
|
|||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let (variables, string_matches) = console.update(cx, |console, cx| {
|
||||
let mut variables = HashMap::default();
|
||||
let mut string_matches = Vec::default();
|
||||
|
@ -354,39 +354,43 @@ impl ConsoleQueryBarCompletionProvider {
|
|||
let query = buffer.read(cx).text();
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
const LIMIT: usize = 10;
|
||||
let matches = fuzzy::match_strings(
|
||||
&string_matches,
|
||||
&query,
|
||||
true,
|
||||
10,
|
||||
LIMIT,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Some(
|
||||
matches
|
||||
.iter()
|
||||
.filter_map(|string_match| {
|
||||
let variable_value = variables.get(&string_match.string)?;
|
||||
let completions = matches
|
||||
.iter()
|
||||
.filter_map(|string_match| {
|
||||
let variable_value = variables.get(&string_match.string)?;
|
||||
|
||||
Some(project::Completion {
|
||||
replace_range: buffer_position..buffer_position,
|
||||
new_text: string_match.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..string_match.string.len(),
|
||||
text: format!("{} {}", string_match.string, variable_value),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
})
|
||||
Some(project::Completion {
|
||||
replace_range: buffer_position..buffer_position,
|
||||
new_text: string_match.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..string_match.string.len(),
|
||||
text: format!("{} {}", string_match.string, variable_value),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(vec![project::CompletionResponse {
|
||||
is_incomplete: completions.len() >= LIMIT,
|
||||
completions,
|
||||
}])
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -396,7 +400,7 @@ impl ConsoleQueryBarCompletionProvider {
|
|||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let completion_task = console.update(cx, |console, cx| {
|
||||
console.session.update(cx, |state, cx| {
|
||||
let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
|
||||
|
@ -411,53 +415,56 @@ impl ConsoleQueryBarCompletionProvider {
|
|||
cx.background_executor().spawn(async move {
|
||||
let completions = completion_task.await?;
|
||||
|
||||
Ok(Some(
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
let new_text = completion
|
||||
.text
|
||||
.as_ref()
|
||||
.unwrap_or(&completion.label)
|
||||
.to_owned();
|
||||
let buffer_text = snapshot.text();
|
||||
let buffer_bytes = buffer_text.as_bytes();
|
||||
let new_bytes = new_text.as_bytes();
|
||||
let completions = completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
let new_text = completion
|
||||
.text
|
||||
.as_ref()
|
||||
.unwrap_or(&completion.label)
|
||||
.to_owned();
|
||||
let buffer_text = snapshot.text();
|
||||
let buffer_bytes = buffer_text.as_bytes();
|
||||
let new_bytes = new_text.as_bytes();
|
||||
|
||||
let mut prefix_len = 0;
|
||||
for i in (0..new_bytes.len()).rev() {
|
||||
if buffer_bytes.ends_with(&new_bytes[0..i]) {
|
||||
prefix_len = i;
|
||||
break;
|
||||
}
|
||||
let mut prefix_len = 0;
|
||||
for i in (0..new_bytes.len()).rev() {
|
||||
if buffer_bytes.ends_with(&new_bytes[0..i]) {
|
||||
prefix_len = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let buffer_offset = buffer_position.to_offset(&snapshot);
|
||||
let start = buffer_offset - prefix_len;
|
||||
let start = snapshot.clip_offset(start, Bias::Left);
|
||||
let start = snapshot.anchor_before(start);
|
||||
let replace_range = start..buffer_position;
|
||||
let buffer_offset = buffer_position.to_offset(&snapshot);
|
||||
let start = buffer_offset - prefix_len;
|
||||
let start = snapshot.clip_offset(start, Bias::Left);
|
||||
let start = snapshot.anchor_before(start);
|
||||
let replace_range = start..buffer_position;
|
||||
|
||||
project::Completion {
|
||||
replace_range,
|
||||
new_text,
|
||||
label: CodeLabel {
|
||||
filter_range: 0..completion.label.len(),
|
||||
text: completion.label,
|
||||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
source: project::CompletionSource::BufferWord {
|
||||
word_range: buffer_position..language::Anchor::MAX,
|
||||
resolved: false,
|
||||
},
|
||||
insert_text_mode: None,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
project::Completion {
|
||||
replace_range,
|
||||
new_text,
|
||||
label: CodeLabel {
|
||||
filter_range: 0..completion.label.len(),
|
||||
text: completion.label,
|
||||
runs: Vec::new(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
source: project::CompletionSource::BufferWord {
|
||||
word_range: buffer_position..language::Anchor::MAX,
|
||||
resolved: false,
|
||||
},
|
||||
insert_text_mode: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(vec![project::CompletionResponse {
|
||||
completions,
|
||||
is_incomplete: false,
|
||||
}])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
|
||||
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list,
|
||||
Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px, uniform_list,
|
||||
};
|
||||
use gpui::{AsyncWindowContext, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
use language::CodeLabel;
|
||||
use language::{Buffer, LanguageName, LanguageRegistry};
|
||||
|
@ -18,6 +17,7 @@ use task::TaskContext;
|
|||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cmp::{Reverse, min},
|
||||
|
@ -47,15 +47,10 @@ pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
|||
// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
|
||||
// documentation not yet being parsed.
|
||||
//
|
||||
// The size of the cache is set to the number of items fetched around the current selection plus one
|
||||
// for the current selection and another to avoid cases where and adjacent selection exits the
|
||||
// cache. The only current benefit of a larger cache would be doing less markdown parsing when the
|
||||
// selection revisits items.
|
||||
//
|
||||
// One future benefit of a larger cache would be reducing flicker on backspace. This would require
|
||||
// not recreating the menu on every change, by not re-querying the language server when
|
||||
// `is_incomplete = false`.
|
||||
const MARKDOWN_CACHE_MAX_SIZE: usize = MARKDOWN_CACHE_BEFORE_ITEMS + MARKDOWN_CACHE_AFTER_ITEMS + 2;
|
||||
// The size of the cache is set to 16, which is roughly 3 times more than the number of items
|
||||
// fetched around the current selection. This way documentation is more often ready for render when
|
||||
// revisiting previous entries, such as when pressing backspace.
|
||||
const MARKDOWN_CACHE_MAX_SIZE: usize = 16;
|
||||
const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
|
||||
const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
|
||||
|
||||
|
@ -197,27 +192,48 @@ pub enum ContextMenuOrigin {
|
|||
QuickActionBar,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CompletionsMenu {
|
||||
pub id: CompletionId,
|
||||
sort_completions: bool,
|
||||
pub initial_position: Anchor,
|
||||
pub initial_query: Option<Arc<String>>,
|
||||
pub is_incomplete: bool,
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
match_candidates: Rc<[StringMatchCandidate]>,
|
||||
pub entries: Rc<RefCell<Vec<StringMatch>>>,
|
||||
match_candidates: Arc<[StringMatchCandidate]>,
|
||||
pub entries: Rc<RefCell<Box<[StringMatch]>>>,
|
||||
pub selected_item: usize,
|
||||
filter_task: Task<()>,
|
||||
cancel_filter: Arc<AtomicBool>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
resolve_completions: bool,
|
||||
show_completion_documentation: bool,
|
||||
pub(super) ignore_completion_provider: bool,
|
||||
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
|
||||
markdown_cache: Rc<RefCell<VecDeque<(usize, Entity<Markdown>)>>>,
|
||||
markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
language: Option<LanguageName>,
|
||||
snippet_sort_order: SnippetSortOrder,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum MarkdownCacheKey {
|
||||
ForCandidate {
|
||||
candidate_id: usize,
|
||||
},
|
||||
ForCompletionMatch {
|
||||
new_text: String,
|
||||
markdown_source: SharedString,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: There should really be a wrapper around fuzzy match tasks that does this.
|
||||
impl Drop for CompletionsMenu {
|
||||
fn drop(&mut self) {
|
||||
self.cancel_filter.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionsMenu {
|
||||
pub fn new(
|
||||
id: CompletionId,
|
||||
|
@ -225,6 +241,8 @@ impl CompletionsMenu {
|
|||
show_completion_documentation: bool,
|
||||
ignore_completion_provider: bool,
|
||||
initial_position: Anchor,
|
||||
initial_query: Option<Arc<String>>,
|
||||
is_incomplete: bool,
|
||||
buffer: Entity<Buffer>,
|
||||
completions: Box<[Completion]>,
|
||||
snippet_sort_order: SnippetSortOrder,
|
||||
|
@ -242,17 +260,21 @@ impl CompletionsMenu {
|
|||
id,
|
||||
sort_completions,
|
||||
initial_position,
|
||||
initial_query,
|
||||
is_incomplete,
|
||||
buffer,
|
||||
show_completion_documentation,
|
||||
ignore_completion_provider,
|
||||
completions: RefCell::new(completions).into(),
|
||||
match_candidates,
|
||||
entries: RefCell::new(Vec::new()).into(),
|
||||
entries: Rc::new(RefCell::new(Box::new([]))),
|
||||
selected_item: 0,
|
||||
filter_task: Task::ready(()),
|
||||
cancel_filter: Arc::new(AtomicBool::new(false)),
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: true,
|
||||
last_rendered_range: RefCell::new(None).into(),
|
||||
markdown_cache: RefCell::new(VecDeque::with_capacity(MARKDOWN_CACHE_MAX_SIZE)).into(),
|
||||
markdown_cache: RefCell::new(VecDeque::new()).into(),
|
||||
language_registry,
|
||||
language,
|
||||
snippet_sort_order,
|
||||
|
@ -303,16 +325,20 @@ impl CompletionsMenu {
|
|||
positions: vec![],
|
||||
string: completion.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
Self {
|
||||
id,
|
||||
sort_completions,
|
||||
initial_position: selection.start,
|
||||
initial_query: None,
|
||||
is_incomplete: false,
|
||||
buffer,
|
||||
completions: RefCell::new(completions).into(),
|
||||
match_candidates,
|
||||
entries: RefCell::new(entries).into(),
|
||||
selected_item: 0,
|
||||
filter_task: Task::ready(()),
|
||||
cancel_filter: Arc::new(AtomicBool::new(false)),
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
resolve_completions: false,
|
||||
show_completion_documentation: false,
|
||||
|
@ -390,14 +416,7 @@ impl CompletionsMenu {
|
|||
) {
|
||||
if self.selected_item != match_index {
|
||||
self.selected_item = match_index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
self.resolve_visible_completions(provider, cx);
|
||||
self.start_markdown_parse_for_nearby_entries(cx);
|
||||
if let Some(provider) = provider {
|
||||
self.handle_selection_changed(provider, window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
self.handle_selection_changed(provider, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,18 +437,25 @@ impl CompletionsMenu {
|
|||
}
|
||||
|
||||
fn handle_selection_changed(
|
||||
&self,
|
||||
provider: &dyn CompletionProvider,
|
||||
&mut self,
|
||||
provider: Option<&dyn CompletionProvider>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let entries = self.entries.borrow();
|
||||
let entry = if self.selected_item < entries.len() {
|
||||
Some(&entries[self.selected_item])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
provider.selection_changed(entry, window, cx);
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
if let Some(provider) = provider {
|
||||
let entries = self.entries.borrow();
|
||||
let entry = if self.selected_item < entries.len() {
|
||||
Some(&entries[self.selected_item])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
provider.selection_changed(entry, window, cx);
|
||||
}
|
||||
self.resolve_visible_completions(provider, cx);
|
||||
self.start_markdown_parse_for_nearby_entries(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn resolve_visible_completions(
|
||||
|
@ -444,6 +470,19 @@ impl CompletionsMenu {
|
|||
return;
|
||||
};
|
||||
|
||||
let entries = self.entries.borrow();
|
||||
if entries.is_empty() {
|
||||
return;
|
||||
}
|
||||
if self.selected_item >= entries.len() {
|
||||
log::error!(
|
||||
"bug: completion selected_item >= entries.len(): {} >= {}",
|
||||
self.selected_item,
|
||||
entries.len()
|
||||
);
|
||||
self.selected_item = entries.len() - 1;
|
||||
}
|
||||
|
||||
// Attempt to resolve completions for every item that will be displayed. This matters
|
||||
// because single line documentation may be displayed inline with the completion.
|
||||
//
|
||||
|
@ -455,7 +494,6 @@ impl CompletionsMenu {
|
|||
let visible_count = last_rendered_range
|
||||
.clone()
|
||||
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
|
||||
let entries = self.entries.borrow();
|
||||
let entry_range = if self.selected_item == 0 {
|
||||
0..min(visible_count, entries.len())
|
||||
} else if self.selected_item == entries.len() - 1 {
|
||||
|
@ -508,11 +546,11 @@ impl CompletionsMenu {
|
|||
.update(cx, |editor, cx| {
|
||||
// `resolve_completions` modified state affecting display.
|
||||
cx.notify();
|
||||
editor.with_completions_menu_matching_id(
|
||||
completion_id,
|
||||
|| (),
|
||||
|this| this.start_markdown_parse_for_nearby_entries(cx),
|
||||
);
|
||||
editor.with_completions_menu_matching_id(completion_id, |menu| {
|
||||
if let Some(menu) = menu {
|
||||
menu.start_markdown_parse_for_nearby_entries(cx)
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
@ -548,11 +586,11 @@ impl CompletionsMenu {
|
|||
return None;
|
||||
}
|
||||
let candidate_id = entries[index].candidate_id;
|
||||
match &self.completions.borrow()[candidate_id].documentation {
|
||||
Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => Some(
|
||||
self.get_or_create_markdown(candidate_id, source.clone(), false, cx)
|
||||
.1,
|
||||
),
|
||||
let completions = self.completions.borrow();
|
||||
match &completions[candidate_id].documentation {
|
||||
Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self
|
||||
.get_or_create_markdown(candidate_id, Some(source), false, &completions, cx)
|
||||
.map(|(_, markdown)| markdown),
|
||||
Some(_) => None,
|
||||
_ => None,
|
||||
}
|
||||
|
@ -561,38 +599,75 @@ impl CompletionsMenu {
|
|||
fn get_or_create_markdown(
|
||||
&self,
|
||||
candidate_id: usize,
|
||||
source: SharedString,
|
||||
source: Option<&SharedString>,
|
||||
is_render: bool,
|
||||
completions: &[Completion],
|
||||
cx: &mut Context<Editor>,
|
||||
) -> (bool, Entity<Markdown>) {
|
||||
) -> Option<(bool, Entity<Markdown>)> {
|
||||
let mut markdown_cache = self.markdown_cache.borrow_mut();
|
||||
if let Some((cache_index, (_, markdown))) = markdown_cache
|
||||
.iter()
|
||||
.find_position(|(id, _)| *id == candidate_id)
|
||||
{
|
||||
let markdown = if is_render && cache_index != 0 {
|
||||
|
||||
let mut has_completion_match_cache_entry = false;
|
||||
let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key {
|
||||
MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id,
|
||||
MarkdownCacheKey::ForCompletionMatch { .. } => {
|
||||
has_completion_match_cache_entry = true;
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if has_completion_match_cache_entry && matching_entry.is_none() {
|
||||
if let Some(source) = source {
|
||||
matching_entry = markdown_cache.iter().find_position(|(key, _)| {
|
||||
matches!(key, MarkdownCacheKey::ForCompletionMatch { markdown_source, .. }
|
||||
if markdown_source == source)
|
||||
});
|
||||
} else {
|
||||
// Heuristic guess that documentation can be reused when new_text matches. This is
|
||||
// to mitigate documentation flicker while typing. If this is wrong, then resolution
|
||||
// should cause the correct documentation to be displayed soon.
|
||||
let completion = &completions[candidate_id];
|
||||
matching_entry = markdown_cache.iter().find_position(|(key, _)| {
|
||||
matches!(key, MarkdownCacheKey::ForCompletionMatch { new_text, .. }
|
||||
if new_text == &completion.new_text)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((cache_index, (key, markdown))) = matching_entry {
|
||||
let markdown = markdown.clone();
|
||||
|
||||
// Since the markdown source matches, the key can now be ForCandidate.
|
||||
if source.is_some() && matches!(key, MarkdownCacheKey::ForCompletionMatch { .. }) {
|
||||
markdown_cache[cache_index].0 = MarkdownCacheKey::ForCandidate { candidate_id };
|
||||
}
|
||||
|
||||
if is_render && cache_index != 0 {
|
||||
// Move the current selection's cache entry to the front.
|
||||
markdown_cache.rotate_right(1);
|
||||
let cache_len = markdown_cache.len();
|
||||
markdown_cache.swap(0, (cache_index + 1) % cache_len);
|
||||
&markdown_cache[0].1
|
||||
} else {
|
||||
markdown
|
||||
};
|
||||
}
|
||||
|
||||
let is_parsing = markdown.update(cx, |markdown, cx| {
|
||||
// `reset` is called as it's possible for documentation to change due to resolve
|
||||
// requests. It does nothing if `source` is unchanged.
|
||||
markdown.reset(source, cx);
|
||||
if let Some(source) = source {
|
||||
// `reset` is called as it's possible for documentation to change due to resolve
|
||||
// requests. It does nothing if `source` is unchanged.
|
||||
markdown.reset(source.clone(), cx);
|
||||
}
|
||||
markdown.is_parsing()
|
||||
});
|
||||
return (is_parsing, markdown.clone());
|
||||
return Some((is_parsing, markdown));
|
||||
}
|
||||
|
||||
let Some(source) = source else {
|
||||
// Can't create markdown as there is no source.
|
||||
return None;
|
||||
};
|
||||
|
||||
if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
|
||||
let markdown = cx.new(|cx| {
|
||||
Markdown::new(
|
||||
source,
|
||||
source.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.language.clone(),
|
||||
cx,
|
||||
|
@ -601,17 +676,20 @@ impl CompletionsMenu {
|
|||
// Handles redraw when the markdown is done parsing. The current render is for a
|
||||
// deferred draw, and so without this did not redraw when `markdown` notified.
|
||||
cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
|
||||
markdown_cache.push_front((candidate_id, markdown.clone()));
|
||||
(true, markdown)
|
||||
markdown_cache.push_front((
|
||||
MarkdownCacheKey::ForCandidate { candidate_id },
|
||||
markdown.clone(),
|
||||
));
|
||||
Some((true, markdown))
|
||||
} else {
|
||||
debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
|
||||
// Moves the last cache entry to the start. The ring buffer is full, so this does no
|
||||
// copying and just shifts indexes.
|
||||
markdown_cache.rotate_right(1);
|
||||
markdown_cache[0].0 = candidate_id;
|
||||
markdown_cache[0].0 = MarkdownCacheKey::ForCandidate { candidate_id };
|
||||
let markdown = &markdown_cache[0].1;
|
||||
markdown.update(cx, |markdown, cx| markdown.reset(source, cx));
|
||||
(true, markdown.clone())
|
||||
markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx));
|
||||
Some((true, markdown.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -774,37 +852,46 @@ impl CompletionsMenu {
|
|||
}
|
||||
|
||||
let mat = &self.entries.borrow()[self.selected_item];
|
||||
let multiline_docs = match self.completions.borrow_mut()[mat.candidate_id]
|
||||
.documentation
|
||||
.as_ref()?
|
||||
{
|
||||
CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()),
|
||||
CompletionDocumentation::SingleLineAndMultiLinePlainText {
|
||||
let completions = self.completions.borrow_mut();
|
||||
let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
|
||||
Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
|
||||
Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
|
||||
plain_text: Some(text),
|
||||
..
|
||||
} => div().child(text.clone()),
|
||||
CompletionDocumentation::MultiLineMarkdown(source) if !source.is_empty() => {
|
||||
let (is_parsing, markdown) =
|
||||
self.get_or_create_markdown(mat.candidate_id, source.clone(), true, cx);
|
||||
if is_parsing {
|
||||
}) => div().child(text.clone()),
|
||||
Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
|
||||
let Some((false, markdown)) = self.get_or_create_markdown(
|
||||
mat.candidate_id,
|
||||
Some(source),
|
||||
true,
|
||||
&completions,
|
||||
cx,
|
||||
) else {
|
||||
return None;
|
||||
}
|
||||
div().child(
|
||||
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
)
|
||||
};
|
||||
Self::render_markdown(markdown, window, cx)
|
||||
}
|
||||
CompletionDocumentation::MultiLineMarkdown(_) => return None,
|
||||
CompletionDocumentation::SingleLine(_) => return None,
|
||||
CompletionDocumentation::Undocumented => return None,
|
||||
CompletionDocumentation::SingleLineAndMultiLinePlainText {
|
||||
plain_text: None, ..
|
||||
} => {
|
||||
None => {
|
||||
// Handle the case where documentation hasn't yet been resolved but there's a
|
||||
// `new_text` match in the cache.
|
||||
//
|
||||
// TODO: It's inconsistent that documentation caching based on matching `new_text`
|
||||
// only works for markdown. Consider generally caching the results of resolving
|
||||
// completions.
|
||||
let Some((false, markdown)) =
|
||||
self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
Self::render_markdown(markdown, window, cx)
|
||||
}
|
||||
Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
|
||||
Some(CompletionDocumentation::SingleLine(_)) => return None,
|
||||
Some(CompletionDocumentation::Undocumented) => return None,
|
||||
Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
|
||||
plain_text: None,
|
||||
..
|
||||
}) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
@ -824,6 +911,177 @@ impl CompletionsMenu {
|
|||
)
|
||||
}
|
||||
|
||||
fn render_markdown(
|
||||
markdown: Entity<Markdown>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Div {
|
||||
div().child(
|
||||
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn filter(
|
||||
&mut self,
|
||||
query: Option<Arc<String>>,
|
||||
provider: Option<Rc<dyn CompletionProvider>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
self.cancel_filter.store(true, Ordering::Relaxed);
|
||||
if let Some(query) = query {
|
||||
self.cancel_filter = Arc::new(AtomicBool::new(false));
|
||||
let matches = self.do_async_filtering(query, cx);
|
||||
let id = self.id;
|
||||
self.filter_task = cx.spawn_in(window, async move |editor, cx| {
|
||||
let matches = matches.await;
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.with_completions_menu_matching_id(id, |this| {
|
||||
if let Some(this) = this {
|
||||
this.set_filter_results(matches, provider, window, cx);
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
} else {
|
||||
self.filter_task = Task::ready(());
|
||||
let matches = self.unfiltered_matches();
|
||||
self.set_filter_results(matches, provider, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_async_filtering(
|
||||
&self,
|
||||
query: Arc<String>,
|
||||
cx: &Context<Editor>,
|
||||
) -> Task<Vec<StringMatch>> {
|
||||
let matches_task = cx.background_spawn({
|
||||
let query = query.clone();
|
||||
let match_candidates = self.match_candidates.clone();
|
||||
let cancel_filter = self.cancel_filter.clone();
|
||||
let background_executor = cx.background_executor().clone();
|
||||
async move {
|
||||
fuzzy::match_strings(
|
||||
&match_candidates,
|
||||
&query,
|
||||
query.chars().any(|c| c.is_uppercase()),
|
||||
100,
|
||||
&cancel_filter,
|
||||
background_executor,
|
||||
)
|
||||
.await
|
||||
}
|
||||
});
|
||||
|
||||
let completions = self.completions.clone();
|
||||
let sort_completions = self.sort_completions;
|
||||
let snippet_sort_order = self.snippet_sort_order;
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let mut matches = matches_task.await;
|
||||
|
||||
if sort_completions {
|
||||
matches = Self::sort_string_matches(
|
||||
matches,
|
||||
Some(&query),
|
||||
snippet_sort_order,
|
||||
completions.borrow().as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
matches
|
||||
})
|
||||
}
|
||||
|
||||
/// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks.
|
||||
pub fn unfiltered_matches(&self) -> Vec<StringMatch> {
|
||||
let mut matches = self
|
||||
.match_candidates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(candidate_id, candidate)| StringMatch {
|
||||
candidate_id,
|
||||
score: Default::default(),
|
||||
positions: Default::default(),
|
||||
string: candidate.string.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if self.sort_completions {
|
||||
matches = Self::sort_string_matches(
|
||||
matches,
|
||||
None,
|
||||
self.snippet_sort_order,
|
||||
self.completions.borrow().as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
matches
|
||||
}
|
||||
|
||||
pub fn set_filter_results(
|
||||
&mut self,
|
||||
matches: Vec<StringMatch>,
|
||||
provider: Option<Rc<dyn CompletionProvider>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
*self.entries.borrow_mut() = matches.into_boxed_slice();
|
||||
self.selected_item = 0;
|
||||
self.handle_selection_changed(provider.as_deref(), window, cx);
|
||||
}
|
||||
|
||||
fn sort_string_matches(
|
||||
matches: Vec<StringMatch>,
|
||||
query: Option<&str>,
|
||||
snippet_sort_order: SnippetSortOrder,
|
||||
completions: &[Completion],
|
||||
) -> Vec<StringMatch> {
|
||||
let mut sortable_items: Vec<SortableMatch<'_>> = matches
|
||||
.into_iter()
|
||||
.map(|string_match| {
|
||||
let completion = &completions[string_match.candidate_id];
|
||||
|
||||
let is_snippet = matches!(
|
||||
&completion.source,
|
||||
CompletionSource::Lsp { lsp_completion, .. }
|
||||
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
|
||||
);
|
||||
|
||||
let sort_text =
|
||||
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
|
||||
lsp_completion.sort_text.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (sort_kind, sort_label) = completion.sort_key();
|
||||
|
||||
SortableMatch {
|
||||
string_match,
|
||||
is_snippet,
|
||||
sort_text,
|
||||
sort_kind,
|
||||
sort_label,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self::sort_matches(&mut sortable_items, query, snippet_sort_order);
|
||||
|
||||
sortable_items
|
||||
.into_iter()
|
||||
.map(|sortable| sortable.string_match)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn sort_matches(
|
||||
matches: &mut Vec<SortableMatch<'_>>,
|
||||
query: Option<&str>,
|
||||
|
@ -857,6 +1115,7 @@ impl CompletionsMenu {
|
|||
let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
|
||||
|
||||
let query_start_lower = query
|
||||
.as_ref()
|
||||
.and_then(|q| q.chars().next())
|
||||
.and_then(|c| c.to_lowercase().next());
|
||||
|
||||
|
@ -890,6 +1149,7 @@ impl CompletionsMenu {
|
|||
};
|
||||
let sort_mixed_case_prefix_length = Reverse(
|
||||
query
|
||||
.as_ref()
|
||||
.map(|q| {
|
||||
q.chars()
|
||||
.zip(mat.string_match.string.chars())
|
||||
|
@ -920,97 +1180,32 @@ impl CompletionsMenu {
|
|||
});
|
||||
}
|
||||
|
||||
pub async fn filter(
|
||||
&mut self,
|
||||
query: Option<&str>,
|
||||
provider: Option<Rc<dyn CompletionProvider>>,
|
||||
editor: WeakEntity<Editor>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) {
|
||||
let mut matches = if let Some(query) = query {
|
||||
fuzzy::match_strings(
|
||||
&self.match_candidates,
|
||||
query,
|
||||
query.chars().any(|c| c.is_uppercase()),
|
||||
100,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
self.match_candidates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(candidate_id, candidate)| StringMatch {
|
||||
candidate_id,
|
||||
score: Default::default(),
|
||||
positions: Default::default(),
|
||||
string: candidate.string.clone(),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
|
||||
self.markdown_cache = prev_menu.markdown_cache.clone();
|
||||
|
||||
if self.sort_completions {
|
||||
let completions = self.completions.borrow();
|
||||
|
||||
let mut sortable_items: Vec<SortableMatch<'_>> = matches
|
||||
.into_iter()
|
||||
.map(|string_match| {
|
||||
let completion = &completions[string_match.candidate_id];
|
||||
|
||||
let is_snippet = matches!(
|
||||
&completion.source,
|
||||
CompletionSource::Lsp { lsp_completion, .. }
|
||||
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
|
||||
);
|
||||
|
||||
let sort_text =
|
||||
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
|
||||
lsp_completion.sort_text.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (sort_kind, sort_label) = completion.sort_key();
|
||||
|
||||
SortableMatch {
|
||||
string_match,
|
||||
is_snippet,
|
||||
sort_text,
|
||||
sort_kind,
|
||||
sort_label,
|
||||
// Convert ForCandidate cache keys to ForCompletionMatch keys.
|
||||
let prev_completions = prev_menu.completions.borrow();
|
||||
self.markdown_cache
|
||||
.borrow_mut()
|
||||
.retain_mut(|(key, _markdown)| match key {
|
||||
MarkdownCacheKey::ForCompletionMatch { .. } => true,
|
||||
MarkdownCacheKey::ForCandidate { candidate_id } => {
|
||||
if let Some(completion) = prev_completions.get(*candidate_id) {
|
||||
match &completion.documentation {
|
||||
Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
|
||||
*key = MarkdownCacheKey::ForCompletionMatch {
|
||||
new_text: completion.new_text.clone(),
|
||||
markdown_source: source.clone(),
|
||||
};
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self::sort_matches(&mut sortable_items, query, self.snippet_sort_order);
|
||||
|
||||
matches = sortable_items
|
||||
.into_iter()
|
||||
.map(|sortable| sortable.string_match)
|
||||
.collect();
|
||||
}
|
||||
|
||||
*self.entries.borrow_mut() = matches;
|
||||
self.selected_item = 0;
|
||||
// This keeps the display consistent when y_flipped.
|
||||
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
|
||||
|
||||
if let Some(provider) = provider {
|
||||
cx.update(|window, cx| {
|
||||
// Since this is async, it's possible the menu has been closed and possibly even
|
||||
// another opened. `provider.selection_changed` should not be called in this case.
|
||||
let this_menu_still_active = editor
|
||||
.read_with(cx, |editor, _cx| {
|
||||
editor.with_completions_menu_matching_id(self.id, || false, |_| true)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if this_menu_still_active {
|
||||
self.handle_selection_changed(&*provider, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ use markdown::Markdown;
|
|||
use mouse_context_menu::MouseContextMenu;
|
||||
use persistence::DB;
|
||||
use project::{
|
||||
BreakpointWithPosition, ProjectPath,
|
||||
BreakpointWithPosition, CompletionResponse, ProjectPath,
|
||||
debugger::{
|
||||
breakpoint_store::{
|
||||
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
|
||||
|
@ -987,7 +987,7 @@ pub struct Editor {
|
|||
context_menu: RefCell<Option<CodeContextMenu>>,
|
||||
context_menu_options: Option<ContextMenuOptions>,
|
||||
mouse_context_menu: Option<MouseContextMenu>,
|
||||
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
|
||||
completion_tasks: Vec<(CompletionId, Task<()>)>,
|
||||
inline_blame_popover: Option<InlineBlamePopover>,
|
||||
signature_help_state: SignatureHelpState,
|
||||
auto_signature_help: Option<bool>,
|
||||
|
@ -1200,7 +1200,7 @@ impl Default for SelectionHistoryMode {
|
|||
|
||||
struct DeferredSelectionEffectsState {
|
||||
changed: bool,
|
||||
show_completions: bool,
|
||||
should_update_completions: bool,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
old_cursor_position: Anchor,
|
||||
history_entry: SelectionHistoryEntry,
|
||||
|
@ -2657,7 +2657,7 @@ impl Editor {
|
|||
&mut self,
|
||||
local: bool,
|
||||
old_cursor_position: &Anchor,
|
||||
show_completions: bool,
|
||||
should_update_completions: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
|
@ -2720,14 +2720,7 @@ impl Editor {
|
|||
|
||||
if local {
|
||||
let new_cursor_position = self.selections.newest_anchor().head();
|
||||
let mut context_menu = self.context_menu.borrow_mut();
|
||||
let completion_menu = match context_menu.as_ref() {
|
||||
Some(CodeContextMenu::Completions(menu)) => Some(menu),
|
||||
_ => {
|
||||
*context_menu = None;
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(buffer_id) = new_cursor_position.buffer_id {
|
||||
if !self.registered_buffers.contains_key(&buffer_id) {
|
||||
if let Some(project) = self.project.as_ref() {
|
||||
|
@ -2744,50 +2737,40 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(completion_menu) = completion_menu {
|
||||
let cursor_position = new_cursor_position.to_offset(buffer);
|
||||
let (word_range, kind) =
|
||||
buffer.surrounding_word(completion_menu.initial_position, true);
|
||||
if kind == Some(CharKind::Word)
|
||||
&& word_range.to_inclusive().contains(&cursor_position)
|
||||
{
|
||||
let mut completion_menu = completion_menu.clone();
|
||||
drop(context_menu);
|
||||
|
||||
let query = Self::completion_query(buffer, cursor_position);
|
||||
let completion_provider = self.completion_provider.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
completion_menu
|
||||
.filter(query.as_deref(), completion_provider, this.clone(), cx)
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let mut context_menu = this.context_menu.borrow_mut();
|
||||
let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if menu.id > completion_menu.id {
|
||||
return;
|
||||
}
|
||||
|
||||
*context_menu = Some(CodeContextMenu::Completions(completion_menu));
|
||||
drop(context_menu);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
|
||||
if show_completions {
|
||||
self.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
}
|
||||
} else {
|
||||
drop(context_menu);
|
||||
self.hide_context_menu(window, cx);
|
||||
let mut context_menu = self.context_menu.borrow_mut();
|
||||
let completion_menu = match context_menu.as_ref() {
|
||||
Some(CodeContextMenu::Completions(menu)) => Some(menu),
|
||||
Some(CodeContextMenu::CodeActions(_)) => {
|
||||
*context_menu = None;
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let completion_position = completion_menu.map(|menu| menu.initial_position);
|
||||
drop(context_menu);
|
||||
|
||||
if should_update_completions {
|
||||
if let Some(completion_position) = completion_position {
|
||||
let new_cursor_offset = new_cursor_position.to_offset(buffer);
|
||||
let position_matches =
|
||||
new_cursor_offset == completion_position.to_offset(buffer);
|
||||
let continue_showing = if position_matches {
|
||||
let (word_range, kind) = buffer.surrounding_word(new_cursor_offset, true);
|
||||
if let Some(CharKind::Word) = kind {
|
||||
word_range.start < new_cursor_offset
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if continue_showing {
|
||||
self.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
} else {
|
||||
self.hide_context_menu(window, cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
drop(context_menu);
|
||||
}
|
||||
|
||||
hide_hover(self, cx);
|
||||
|
@ -2981,7 +2964,7 @@ impl Editor {
|
|||
self.change_selections_inner(true, autoscroll, window, cx, change)
|
||||
}
|
||||
|
||||
pub(crate) fn change_selections_without_showing_completions<R>(
|
||||
pub(crate) fn change_selections_without_updating_completions<R>(
|
||||
&mut self,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
window: &mut Window,
|
||||
|
@ -2993,7 +2976,7 @@ impl Editor {
|
|||
|
||||
fn change_selections_inner<R>(
|
||||
&mut self,
|
||||
show_completions: bool,
|
||||
should_update_completions: bool,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -3001,14 +2984,14 @@ impl Editor {
|
|||
) -> R {
|
||||
if let Some(state) = &mut self.deferred_selection_effects_state {
|
||||
state.autoscroll = autoscroll.or(state.autoscroll);
|
||||
state.show_completions = show_completions;
|
||||
state.should_update_completions = should_update_completions;
|
||||
let (changed, result) = self.selections.change_with(cx, change);
|
||||
state.changed |= changed;
|
||||
return result;
|
||||
}
|
||||
let mut state = DeferredSelectionEffectsState {
|
||||
changed: false,
|
||||
show_completions,
|
||||
should_update_completions,
|
||||
autoscroll,
|
||||
old_cursor_position: self.selections.newest_anchor().head(),
|
||||
history_entry: SelectionHistoryEntry {
|
||||
|
@ -3068,7 +3051,7 @@ impl Editor {
|
|||
self.selections_did_change(
|
||||
true,
|
||||
&old_cursor_position,
|
||||
state.show_completions,
|
||||
state.should_update_completions,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
@ -3979,7 +3962,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
let had_active_inline_completion = this.has_active_inline_completion();
|
||||
this.change_selections_without_showing_completions(
|
||||
this.change_selections_without_updating_completions(
|
||||
Some(Autoscroll::fit()),
|
||||
window,
|
||||
cx,
|
||||
|
@ -5025,7 +5008,7 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.open_completions_menu(true, None, window, cx);
|
||||
self.open_or_update_completions_menu(true, None, window, cx);
|
||||
}
|
||||
|
||||
pub fn show_completions(
|
||||
|
@ -5034,10 +5017,10 @@ impl Editor {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.open_completions_menu(false, options.trigger.as_deref(), window, cx);
|
||||
self.open_or_update_completions_menu(false, options.trigger.as_deref(), window, cx);
|
||||
}
|
||||
|
||||
fn open_completions_menu(
|
||||
fn open_or_update_completions_menu(
|
||||
&mut self,
|
||||
ignore_completion_provider: bool,
|
||||
trigger: Option<&str>,
|
||||
|
@ -5047,9 +5030,6 @@ impl Editor {
|
|||
if self.pending_rename.is_some() {
|
||||
return;
|
||||
}
|
||||
if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let position = self.selections.newest_anchor().head();
|
||||
if position.diff_base_anchor.is_some() {
|
||||
|
@ -5062,11 +5042,52 @@ impl Editor {
|
|||
return;
|
||||
};
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let show_completion_documentation = buffer_snapshot
|
||||
.settings_at(buffer_position, cx)
|
||||
.show_completion_documentation;
|
||||
|
||||
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
|
||||
let query: Option<Arc<String>> =
|
||||
Self::completion_query(&self.buffer.read(cx).read(cx), position)
|
||||
.map(|query| query.into());
|
||||
|
||||
let provider = if ignore_completion_provider {
|
||||
None
|
||||
} else {
|
||||
self.completion_provider.clone()
|
||||
};
|
||||
|
||||
let sort_completions = provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.sort_completions());
|
||||
|
||||
let filter_completions = provider
|
||||
.as_ref()
|
||||
.map_or(true, |provider| provider.filter_completions());
|
||||
|
||||
// When `is_incomplete` is false, can filter completions instead of re-querying when the
|
||||
// current query is a suffix of the initial query.
|
||||
if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
|
||||
if !menu.is_incomplete && filter_completions {
|
||||
// If the new query is a suffix of the old query (typing more characters) and
|
||||
// the previous result was complete, the existing completions can be filtered.
|
||||
//
|
||||
// Note that this is always true for snippet completions.
|
||||
let query_matches = match (&menu.initial_query, &query) {
|
||||
(Some(initial_query), Some(query)) => query.starts_with(initial_query.as_ref()),
|
||||
(None, _) => true,
|
||||
_ => false,
|
||||
};
|
||||
if query_matches {
|
||||
let position_matches = if menu.initial_position == position {
|
||||
true
|
||||
} else {
|
||||
let snapshot = self.buffer.read(cx).read(cx);
|
||||
menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)
|
||||
};
|
||||
if position_matches {
|
||||
menu.filter(query.clone(), provider.clone(), window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let trigger_kind = match trigger {
|
||||
Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
|
||||
|
@ -5085,14 +5106,14 @@ impl Editor {
|
|||
trigger_kind,
|
||||
};
|
||||
|
||||
let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
|
||||
let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
|
||||
let (replace_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
|
||||
let (replace_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
|
||||
let word_to_exclude = buffer_snapshot
|
||||
.text_for_range(old_range.clone())
|
||||
.text_for_range(replace_range.clone())
|
||||
.collect::<String>();
|
||||
(
|
||||
buffer_snapshot.anchor_before(old_range.start)
|
||||
..buffer_snapshot.anchor_after(old_range.end),
|
||||
buffer_snapshot.anchor_before(replace_range.start)
|
||||
..buffer_snapshot.anchor_after(replace_range.end),
|
||||
Some(word_to_exclude),
|
||||
)
|
||||
} else {
|
||||
|
@ -5106,6 +5127,10 @@ impl Editor {
|
|||
let completion_settings =
|
||||
language_settings(language.clone(), buffer_snapshot.file(), cx).completions;
|
||||
|
||||
let show_completion_documentation = buffer_snapshot
|
||||
.settings_at(buffer_position, cx)
|
||||
.show_completion_documentation;
|
||||
|
||||
// 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;
|
||||
|
@ -5121,18 +5146,13 @@ impl Editor {
|
|||
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
|
||||
..buffer_snapshot.point_to_offset(max_word_search);
|
||||
|
||||
let provider = if ignore_completion_provider {
|
||||
None
|
||||
} else {
|
||||
self.completion_provider.clone()
|
||||
};
|
||||
let skip_digits = query
|
||||
.as_ref()
|
||||
.map_or(true, |query| !query.chars().any(|c| c.is_digit(10)));
|
||||
|
||||
let (mut words, provided_completions) = match &provider {
|
||||
let (mut words, provider_responses) = match &provider {
|
||||
Some(provider) => {
|
||||
let completions = provider.completions(
|
||||
let provider_responses = provider.completions(
|
||||
position.excerpt_id,
|
||||
&buffer,
|
||||
buffer_position,
|
||||
|
@ -5153,7 +5173,7 @@ impl Editor {
|
|||
}),
|
||||
};
|
||||
|
||||
(words, completions)
|
||||
(words, provider_responses)
|
||||
}
|
||||
None => (
|
||||
cx.background_spawn(async move {
|
||||
|
@ -5163,137 +5183,165 @@ impl Editor {
|
|||
skip_digits,
|
||||
})
|
||||
}),
|
||||
Task::ready(Ok(None)),
|
||||
Task::ready(Ok(Vec::new())),
|
||||
),
|
||||
};
|
||||
|
||||
let sort_completions = provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.sort_completions());
|
||||
|
||||
let filter_completions = provider
|
||||
.as_ref()
|
||||
.map_or(true, |provider| provider.filter_completions());
|
||||
|
||||
let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order;
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn_in(window, async move |editor, cx| {
|
||||
async move {
|
||||
editor.update(cx, |this, _| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
})?;
|
||||
let Ok(()) = editor.update(cx, |this, _| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut completions = Vec::new();
|
||||
if let Some(provided_completions) = provided_completions.await.log_err().flatten() {
|
||||
completions.extend(provided_completions);
|
||||
// TODO: Ideally completions from different sources would be selectively re-queried, so
|
||||
// that having one source with `is_incomplete: true` doesn't cause all to be re-queried.
|
||||
let mut completions = Vec::new();
|
||||
let mut is_incomplete = false;
|
||||
if let Some(provider_responses) = provider_responses.await.log_err() {
|
||||
if !provider_responses.is_empty() {
|
||||
for response in provider_responses {
|
||||
completions.extend(response.completions);
|
||||
is_incomplete = is_incomplete || response.is_incomplete;
|
||||
}
|
||||
if completion_settings.words == WordsCompletionMode::Fallback {
|
||||
words = Task::ready(BTreeMap::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut words = words.await;
|
||||
if let Some(word_to_exclude) = &word_to_exclude {
|
||||
words.remove(word_to_exclude);
|
||||
}
|
||||
for lsp_completion in &completions {
|
||||
words.remove(&lsp_completion.new_text);
|
||||
}
|
||||
completions.extend(words.into_iter().map(|(word, word_range)| Completion {
|
||||
replace_range: old_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
insert_text_mode: Some(InsertTextMode::AS_IS),
|
||||
confirm: None,
|
||||
}));
|
||||
let mut words = words.await;
|
||||
if let Some(word_to_exclude) = &word_to_exclude {
|
||||
words.remove(word_to_exclude);
|
||||
}
|
||||
for lsp_completion in &completions {
|
||||
words.remove(&lsp_completion.new_text);
|
||||
}
|
||||
completions.extend(words.into_iter().map(|(word, word_range)| Completion {
|
||||
replace_range: replace_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
word_range,
|
||||
resolved: false,
|
||||
},
|
||||
insert_text_mode: Some(InsertTextMode::AS_IS),
|
||||
confirm: None,
|
||||
}));
|
||||
|
||||
let menu = if completions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut menu = editor.update(cx, |editor, cx| {
|
||||
let languages = editor
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|(workspace, _)| workspace.upgrade())
|
||||
.map(|workspace| workspace.read(cx).app_state().languages.clone());
|
||||
CompletionsMenu::new(
|
||||
id,
|
||||
sort_completions,
|
||||
show_completion_documentation,
|
||||
ignore_completion_provider,
|
||||
position,
|
||||
buffer.clone(),
|
||||
completions.into(),
|
||||
snippet_sort_order,
|
||||
languages,
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
menu.filter(
|
||||
if filter_completions {
|
||||
query.as_deref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
provider,
|
||||
editor.clone(),
|
||||
let menu = if completions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let Ok((mut menu, matches_task)) = editor.update(cx, |editor, cx| {
|
||||
let languages = editor
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|(workspace, _)| workspace.upgrade())
|
||||
.map(|workspace| workspace.read(cx).app_state().languages.clone());
|
||||
let menu = CompletionsMenu::new(
|
||||
id,
|
||||
sort_completions,
|
||||
show_completion_documentation,
|
||||
ignore_completion_provider,
|
||||
position,
|
||||
query.clone(),
|
||||
is_incomplete,
|
||||
buffer.clone(),
|
||||
completions.into(),
|
||||
snippet_sort_order,
|
||||
languages,
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
);
|
||||
|
||||
menu.visible().then_some(menu)
|
||||
let query = if filter_completions { query } else { None };
|
||||
let matches_task = if let Some(query) = query {
|
||||
menu.do_async_filtering(query, cx)
|
||||
} else {
|
||||
Task::ready(menu.unfiltered_matches())
|
||||
};
|
||||
(menu, matches_task)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
let matches = matches_task.await;
|
||||
|
||||
let Ok(()) = editor.update_in(cx, |editor, window, cx| {
|
||||
// Newer menu already set, so exit.
|
||||
match editor.context_menu.borrow().as_ref() {
|
||||
None => {}
|
||||
Some(CodeContextMenu::Completions(prev_menu)) => {
|
||||
if prev_menu.id > id {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
_ => {}
|
||||
};
|
||||
|
||||
// Only valid to take prev_menu because it the new menu is immediately set
|
||||
// below, or the menu is hidden.
|
||||
match editor.context_menu.borrow_mut().take() {
|
||||
Some(CodeContextMenu::Completions(prev_menu)) => {
|
||||
let position_matches =
|
||||
if prev_menu.initial_position == menu.initial_position {
|
||||
true
|
||||
} else {
|
||||
let snapshot = editor.buffer.read(cx).read(cx);
|
||||
prev_menu.initial_position.to_offset(&snapshot)
|
||||
== menu.initial_position.to_offset(&snapshot)
|
||||
};
|
||||
if position_matches {
|
||||
// Preserve markdown cache before `set_filter_results` because it will
|
||||
// try to populate the documentation cache.
|
||||
menu.preserve_markdown_cache(prev_menu);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
menu.set_filter_results(matches, provider, window, cx);
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
menu.visible().then_some(menu)
|
||||
};
|
||||
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
if editor.focus_handle.is_focused(window) {
|
||||
if let Some(menu) = menu {
|
||||
*editor.context_menu.borrow_mut() =
|
||||
Some(CodeContextMenu::Completions(menu));
|
||||
|
||||
crate::hover_popover::hide_hover(editor, cx);
|
||||
if editor.show_edit_predictions_in_menu() {
|
||||
editor.update_visible_inline_completion(window, cx);
|
||||
} else {
|
||||
editor.discard_inline_completion(false, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if editor.focus_handle.is_focused(window) && menu.is_some() {
|
||||
let mut menu = menu.unwrap();
|
||||
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
|
||||
crate::hover_popover::hide_hover(editor, cx);
|
||||
*editor.context_menu.borrow_mut() =
|
||||
Some(CodeContextMenu::Completions(menu));
|
||||
|
||||
if editor.show_edit_predictions_in_menu() {
|
||||
editor.update_visible_inline_completion(window, cx);
|
||||
} else {
|
||||
editor.discard_inline_completion(false, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
} else if editor.completion_tasks.len() <= 1 {
|
||||
// If there are no more completion tasks and the last menu was
|
||||
// empty, we should hide it.
|
||||
if editor.completion_tasks.len() <= 1 {
|
||||
// If there are no more completion tasks and the last menu was empty, we should hide it.
|
||||
let was_hidden = editor.hide_context_menu(window, cx).is_none();
|
||||
// If it was already hidden and we don't show inline
|
||||
// completions in the menu, we should also show the
|
||||
// inline-completion when available.
|
||||
// If it was already hidden and we don't show inline completions in the menu, we should
|
||||
// also show the inline-completion when available.
|
||||
if was_hidden && editor.show_edit_predictions_in_menu() {
|
||||
editor.update_visible_inline_completion(window, cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
self.completion_tasks.push((id, task));
|
||||
|
@ -5313,17 +5361,16 @@ impl Editor {
|
|||
pub fn with_completions_menu_matching_id<R>(
|
||||
&self,
|
||||
id: CompletionId,
|
||||
on_absent: impl FnOnce() -> R,
|
||||
on_match: impl FnOnce(&mut CompletionsMenu) -> R,
|
||||
f: impl FnOnce(Option<&mut CompletionsMenu>) -> R,
|
||||
) -> R {
|
||||
let mut context_menu = self.context_menu.borrow_mut();
|
||||
let Some(CodeContextMenu::Completions(completions_menu)) = &mut *context_menu else {
|
||||
return on_absent();
|
||||
return f(None);
|
||||
};
|
||||
if completions_menu.id != id {
|
||||
return on_absent();
|
||||
return f(None);
|
||||
}
|
||||
on_match(completions_menu)
|
||||
f(Some(completions_menu))
|
||||
}
|
||||
|
||||
pub fn confirm_completion(
|
||||
|
@ -5396,7 +5443,7 @@ impl Editor {
|
|||
.clone();
|
||||
cx.stop_propagation();
|
||||
|
||||
let buffer_handle = completions_menu.buffer;
|
||||
let buffer_handle = completions_menu.buffer.clone();
|
||||
|
||||
let CompletionEdit {
|
||||
new_text,
|
||||
|
@ -20206,7 +20253,7 @@ pub trait CompletionProvider {
|
|||
trigger: CompletionContext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>>;
|
||||
) -> Task<Result<Vec<CompletionResponse>>>;
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
|
@ -20315,7 +20362,7 @@ fn snippet_completions(
|
|||
buffer: &Entity<Buffer>,
|
||||
buffer_position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<Completion>>> {
|
||||
) -> Task<Result<CompletionResponse>> {
|
||||
let languages = buffer.read(cx).languages_at(buffer_position);
|
||||
let snippet_store = project.snippets().read(cx);
|
||||
|
||||
|
@ -20334,7 +20381,10 @@ fn snippet_completions(
|
|||
.collect();
|
||||
|
||||
if scopes.is_empty() {
|
||||
return Task::ready(Ok(vec![]));
|
||||
return Task::ready(Ok(CompletionResponse {
|
||||
completions: vec![],
|
||||
is_incomplete: false,
|
||||
}));
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).text_snapshot();
|
||||
|
@ -20344,7 +20394,8 @@ fn snippet_completions(
|
|||
let executor = cx.background_executor().clone();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let mut all_results: Vec<Completion> = Vec::new();
|
||||
let mut is_incomplete = false;
|
||||
let mut completions: Vec<Completion> = Vec::new();
|
||||
for (scope, snippets) in scopes.into_iter() {
|
||||
let classifier = CharClassifier::new(Some(scope)).for_completion(true);
|
||||
let mut last_word = chars
|
||||
|
@ -20354,7 +20405,10 @@ fn snippet_completions(
|
|||
last_word = last_word.chars().rev().collect();
|
||||
|
||||
if last_word.is_empty() {
|
||||
return Ok(vec![]);
|
||||
return Ok(CompletionResponse {
|
||||
completions: vec![],
|
||||
is_incomplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot);
|
||||
|
@ -20375,16 +20429,21 @@ fn snippet_completions(
|
|||
})
|
||||
.collect::<Vec<StringMatchCandidate>>();
|
||||
|
||||
const MAX_RESULTS: usize = 100;
|
||||
let mut matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&last_word,
|
||||
last_word.chars().any(|c| c.is_uppercase()),
|
||||
100,
|
||||
MAX_RESULTS,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if matches.len() >= MAX_RESULTS {
|
||||
is_incomplete = true;
|
||||
}
|
||||
|
||||
// Remove all candidates where the query's start does not match the start of any word in the candidate
|
||||
if let Some(query_start) = last_word.chars().next() {
|
||||
matches.retain(|string_match| {
|
||||
|
@ -20404,76 +20463,72 @@ fn snippet_completions(
|
|||
.map(|m| m.string)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut result: Vec<Completion> = snippets
|
||||
.iter()
|
||||
.filter_map(|snippet| {
|
||||
let matching_prefix = snippet
|
||||
.prefix
|
||||
.iter()
|
||||
.find(|prefix| matched_strings.contains(*prefix))?;
|
||||
let start = as_offset - last_word.len();
|
||||
let start = snapshot.anchor_before(start);
|
||||
let range = start..buffer_position;
|
||||
let lsp_start = to_lsp(&start);
|
||||
let lsp_range = lsp::Range {
|
||||
start: lsp_start,
|
||||
end: lsp_end,
|
||||
};
|
||||
Some(Completion {
|
||||
replace_range: range,
|
||||
new_text: snippet.body.clone(),
|
||||
source: CompletionSource::Lsp {
|
||||
insert_range: None,
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
resolved: true,
|
||||
lsp_completion: Box::new(lsp::CompletionItem {
|
||||
label: snippet.prefix.first().unwrap().clone(),
|
||||
kind: Some(CompletionItemKind::SNIPPET),
|
||||
label_details: snippet.description.as_ref().map(|description| {
|
||||
lsp::CompletionItemLabelDetails {
|
||||
detail: Some(description.clone()),
|
||||
description: None,
|
||||
}
|
||||
}),
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: snippet.body.clone(),
|
||||
insert: lsp_range,
|
||||
replace: lsp_range,
|
||||
},
|
||||
)),
|
||||
filter_text: Some(snippet.body.clone()),
|
||||
sort_text: Some(char::MAX.to_string()),
|
||||
..lsp::CompletionItem::default()
|
||||
completions.extend(snippets.iter().filter_map(|snippet| {
|
||||
let matching_prefix = snippet
|
||||
.prefix
|
||||
.iter()
|
||||
.find(|prefix| matched_strings.contains(*prefix))?;
|
||||
let start = as_offset - last_word.len();
|
||||
let start = snapshot.anchor_before(start);
|
||||
let range = start..buffer_position;
|
||||
let lsp_start = to_lsp(&start);
|
||||
let lsp_range = lsp::Range {
|
||||
start: lsp_start,
|
||||
end: lsp_end,
|
||||
};
|
||||
Some(Completion {
|
||||
replace_range: range,
|
||||
new_text: snippet.body.clone(),
|
||||
source: CompletionSource::Lsp {
|
||||
insert_range: None,
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
resolved: true,
|
||||
lsp_completion: Box::new(lsp::CompletionItem {
|
||||
label: snippet.prefix.first().unwrap().clone(),
|
||||
kind: Some(CompletionItemKind::SNIPPET),
|
||||
label_details: snippet.description.as_ref().map(|description| {
|
||||
lsp::CompletionItemLabelDetails {
|
||||
detail: Some(description.clone()),
|
||||
description: None,
|
||||
}
|
||||
}),
|
||||
lsp_defaults: None,
|
||||
},
|
||||
label: CodeLabel {
|
||||
text: matching_prefix.clone(),
|
||||
runs: Vec::new(),
|
||||
filter_range: 0..matching_prefix.len(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: Some(
|
||||
CompletionDocumentation::SingleLineAndMultiLinePlainText {
|
||||
single_line: snippet.name.clone().into(),
|
||||
plain_text: snippet
|
||||
.description
|
||||
.clone()
|
||||
.map(|description| description.into()),
|
||||
},
|
||||
),
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
})
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||
lsp::InsertReplaceEdit {
|
||||
new_text: snippet.body.clone(),
|
||||
insert: lsp_range,
|
||||
replace: lsp_range,
|
||||
},
|
||||
)),
|
||||
filter_text: Some(snippet.body.clone()),
|
||||
sort_text: Some(char::MAX.to_string()),
|
||||
..lsp::CompletionItem::default()
|
||||
}),
|
||||
lsp_defaults: None,
|
||||
},
|
||||
label: CodeLabel {
|
||||
text: matching_prefix.clone(),
|
||||
runs: Vec::new(),
|
||||
filter_range: 0..matching_prefix.len(),
|
||||
},
|
||||
icon_path: None,
|
||||
documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
|
||||
single_line: snippet.name.clone().into(),
|
||||
plain_text: snippet
|
||||
.description
|
||||
.clone()
|
||||
.map(|description| description.into()),
|
||||
}),
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
all_results.append(&mut result);
|
||||
}))
|
||||
}
|
||||
|
||||
Ok(all_results)
|
||||
Ok(CompletionResponse {
|
||||
completions,
|
||||
is_incomplete,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -20486,25 +20541,17 @@ impl CompletionProvider for Entity<Project> {
|
|||
options: CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
let snippets = snippet_completions(project, buffer, buffer_position, cx);
|
||||
let project_completions = project.completions(buffer, buffer_position, options, cx);
|
||||
cx.background_spawn(async move {
|
||||
let snippets_completions = snippets.await?;
|
||||
match project_completions.await? {
|
||||
Some(mut completions) => {
|
||||
completions.extend(snippets_completions);
|
||||
Ok(Some(completions))
|
||||
}
|
||||
None => {
|
||||
if snippets_completions.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(snippets_completions))
|
||||
}
|
||||
}
|
||||
let mut responses = project_completions.await?;
|
||||
let snippets = snippets.await?;
|
||||
if !snippets.completions.is_empty() {
|
||||
responses.push(snippets);
|
||||
}
|
||||
Ok(responses)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::*;
|
||||
use crate::{
|
||||
JoinLines,
|
||||
code_context_menus::CodeContextMenu,
|
||||
inline_completion_tests::FakeInlineCompletionProvider,
|
||||
linked_editing_ranges::LinkedEditingRanges,
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
|
@ -11184,14 +11185,15 @@ async fn test_completion(cx: &mut TestAppContext) {
|
|||
"});
|
||||
cx.simulate_keystroke(".");
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three
|
||||
"},
|
||||
vec!["first_completion", "second_completion"],
|
||||
true,
|
||||
counter.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
|
@ -11291,7 +11293,6 @@ async fn test_completion(cx: &mut TestAppContext) {
|
|||
additional edit
|
||||
"});
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two s
|
||||
|
@ -11299,7 +11300,9 @@ async fn test_completion(cx: &mut TestAppContext) {
|
|||
additional edit
|
||||
"},
|
||||
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
|
||||
true,
|
||||
counter.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
|
@ -11309,7 +11312,6 @@ async fn test_completion(cx: &mut TestAppContext) {
|
|||
cx.simulate_keystroke("i");
|
||||
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two si
|
||||
|
@ -11317,7 +11319,9 @@ async fn test_completion(cx: &mut TestAppContext) {
|
|||
additional edit
|
||||
"},
|
||||
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
|
||||
true,
|
||||
counter.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
|
@ -11351,10 +11355,11 @@ async fn test_completion(cx: &mut TestAppContext) {
|
|||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
"editor.<clo|>",
|
||||
vec!["close", "clobber"],
|
||||
true,
|
||||
counter.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
|
@ -11371,6 +11376,128 @@ async fn test_completion(cx: &mut TestAppContext) {
|
|||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_reuse(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
cx.set_state("objˇ");
|
||||
cx.simulate_keystroke(".");
|
||||
|
||||
// Initial completion request returns complete results
|
||||
let is_incomplete = false;
|
||||
handle_completion_request(
|
||||
"obj.|<>",
|
||||
vec!["a", "ab", "abc"],
|
||||
is_incomplete,
|
||||
counter.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
cx.assert_editor_state("obj.ˇ");
|
||||
check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
|
||||
|
||||
// Type "a" - filters existing completions
|
||||
cx.simulate_keystroke("a");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
cx.assert_editor_state("obj.aˇ");
|
||||
check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
|
||||
|
||||
// Type "b" - filters existing completions
|
||||
cx.simulate_keystroke("b");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
cx.assert_editor_state("obj.abˇ");
|
||||
check_displayed_completions(vec!["ab", "abc"], &mut cx);
|
||||
|
||||
// Type "c" - filters existing completions
|
||||
cx.simulate_keystroke("c");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
cx.assert_editor_state("obj.abcˇ");
|
||||
check_displayed_completions(vec!["abc"], &mut cx);
|
||||
|
||||
// Backspace to delete "c" - filters existing completions
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.backspace(&Backspace, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
cx.assert_editor_state("obj.abˇ");
|
||||
check_displayed_completions(vec!["ab", "abc"], &mut cx);
|
||||
|
||||
// Moving cursor to the left dismisses menu.
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_left(&MoveLeft, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
cx.assert_editor_state("obj.aˇb");
|
||||
cx.update_editor(|editor, _, _| {
|
||||
assert_eq!(editor.context_menu_visible(), false);
|
||||
});
|
||||
|
||||
// Type "b" - new request
|
||||
cx.simulate_keystroke("b");
|
||||
let is_incomplete = false;
|
||||
handle_completion_request(
|
||||
"obj.<ab|>a",
|
||||
vec!["ab", "abc"],
|
||||
is_incomplete,
|
||||
counter.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
|
||||
cx.assert_editor_state("obj.abˇb");
|
||||
check_displayed_completions(vec!["ab", "abc"], &mut cx);
|
||||
|
||||
// Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.backspace(&Backspace, window, cx);
|
||||
});
|
||||
let is_incomplete = false;
|
||||
handle_completion_request(
|
||||
"obj.<a|>b",
|
||||
vec!["a", "ab", "abc"],
|
||||
is_incomplete,
|
||||
counter.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
|
||||
cx.assert_editor_state("obj.aˇb");
|
||||
check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
|
||||
|
||||
// Backspace to delete "a" - dismisses menu.
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.backspace(&Backspace, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
|
||||
cx.assert_editor_state("obj.ˇb");
|
||||
cx.update_editor(|editor, _, _| {
|
||||
assert_eq!(editor.context_menu_visible(), false);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_word_completion(cx: &mut TestAppContext) {
|
||||
let lsp_fetch_timeout_ms = 10;
|
||||
|
@ -12051,9 +12178,11 @@ async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
|
|||
let task_completion_item = closure_completion_item.clone();
|
||||
counter_clone.fetch_add(1, atomic::Ordering::Release);
|
||||
async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
task_completion_item,
|
||||
])))
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
is_incomplete: true,
|
||||
item_defaults: None,
|
||||
items: vec![task_completion_item],
|
||||
})))
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -21109,6 +21238,22 @@ pub fn handle_signature_help_request(
|
|||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
|
||||
cx.update_editor(|editor, _, _| {
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
|
||||
let entries = menu.entries.borrow();
|
||||
let entries = entries
|
||||
.iter()
|
||||
.map(|entry| entry.string.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(entries, expected);
|
||||
} else {
|
||||
panic!("Expected completions menu");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
@ -21116,10 +21261,11 @@ pub fn handle_signature_help_request(
|
|||
/// Also see `handle_completion_request_with_insert_and_replace`.
|
||||
#[track_caller]
|
||||
pub fn handle_completion_request(
|
||||
cx: &mut EditorLspTestContext,
|
||||
marked_string: &str,
|
||||
completions: Vec<&'static str>,
|
||||
is_incomplete: bool,
|
||||
counter: Arc<AtomicUsize>,
|
||||
cx: &mut EditorLspTestContext,
|
||||
) -> impl Future<Output = ()> {
|
||||
let complete_from_marker: TextRangeMarker = '|'.into();
|
||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||
|
@ -21143,8 +21289,10 @@ pub fn handle_completion_request(
|
|||
params.text_document_position.position,
|
||||
complete_from_position
|
||||
);
|
||||
Ok(Some(lsp::CompletionResponse::Array(
|
||||
completions
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
is_incomplete: is_incomplete,
|
||||
item_defaults: None,
|
||||
items: completions
|
||||
.iter()
|
||||
.map(|completion_text| lsp::CompletionItem {
|
||||
label: completion_text.to_string(),
|
||||
|
@ -21155,7 +21303,7 @@ pub fn handle_completion_request(
|
|||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
)))
|
||||
})))
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1095,14 +1095,15 @@ mod tests {
|
|||
//prompt autocompletion menu
|
||||
cx.simulate_keystroke(".");
|
||||
handle_completion_request(
|
||||
&mut cx,
|
||||
indoc! {"
|
||||
one.|<>
|
||||
two
|
||||
three
|
||||
"},
|
||||
vec!["first_completion", "second_completion"],
|
||||
true,
|
||||
counter.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible
|
||||
|
|
|
@ -600,7 +600,7 @@ pub(crate) fn handle_from(
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.change_selections_without_showing_completions(None, window, cx, |s| {
|
||||
this.change_selections_without_updating_completions(None, window, cx, |s| {
|
||||
s.select(base_selections);
|
||||
});
|
||||
})
|
||||
|
|
|
@ -759,8 +759,8 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
|||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.flat_map(|response| response.completions)
|
||||
.map(|c| c.label.text)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
|
|
|
@ -11,7 +11,7 @@ use language::{
|
|||
DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
|
||||
};
|
||||
use project::lsp_store::CompletionDocumentation;
|
||||
use project::{Completion, CompletionSource, Project, ProjectPath};
|
||||
use project::{Completion, CompletionResponse, CompletionSource, Project, ProjectPath};
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Write as _;
|
||||
use std::ops::Range;
|
||||
|
@ -641,18 +641,18 @@ impl CompletionProvider for RustStyleCompletionProvider {
|
|||
_: editor::CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let Some(replace_range) = completion_replace_range(&buffer.read(cx).snapshot(), &position)
|
||||
else {
|
||||
return Task::ready(Ok(Some(Vec::new())));
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
self.div_inspector.update(cx, |div_inspector, _cx| {
|
||||
div_inspector.rust_completion_replace_range = Some(replace_range.clone());
|
||||
});
|
||||
|
||||
Task::ready(Ok(Some(
|
||||
STYLE_METHODS
|
||||
Task::ready(Ok(vec![CompletionResponse {
|
||||
completions: STYLE_METHODS
|
||||
.iter()
|
||||
.map(|(_, method)| Completion {
|
||||
replace_range: replace_range.clone(),
|
||||
|
@ -667,7 +667,8 @@ impl CompletionProvider for RustStyleCompletionProvider {
|
|||
confirm: None,
|
||||
})
|
||||
.collect(),
|
||||
)))
|
||||
is_incomplete: false,
|
||||
}]))
|
||||
}
|
||||
|
||||
fn resolve_completions(
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
mod signature_help;
|
||||
|
||||
use crate::{
|
||||
CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, DocumentSymbol, Hover,
|
||||
HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart,
|
||||
InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent,
|
||||
PrepareRenameResponse, ProjectTransaction, ResolveState,
|
||||
CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentHighlight,
|
||||
DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
|
||||
InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
|
||||
LspAction, MarkupContent, PrepareRenameResponse, ProjectTransaction, ResolveState,
|
||||
lsp_store::{LocalLspStore, LspStore},
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
|
@ -2095,7 +2095,7 @@ impl LspCommand for GetHover {
|
|||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetCompletions {
|
||||
type Response = Vec<CoreCompletion>;
|
||||
type Response = CoreCompletionResponse;
|
||||
type LspRequest = lsp::request::Completion;
|
||||
type ProtoRequest = proto::GetCompletions;
|
||||
|
||||
|
@ -2127,19 +2127,22 @@ impl LspCommand for GetCompletions {
|
|||
mut cx: AsyncApp,
|
||||
) -> Result<Self::Response> {
|
||||
let mut response_list = None;
|
||||
let mut completions = if let Some(completions) = completions {
|
||||
let (mut completions, mut is_incomplete) = if let Some(completions) = completions {
|
||||
match completions {
|
||||
lsp::CompletionResponse::Array(completions) => completions,
|
||||
lsp::CompletionResponse::Array(completions) => (completions, false),
|
||||
lsp::CompletionResponse::List(mut list) => {
|
||||
let is_incomplete = list.is_incomplete;
|
||||
let items = std::mem::take(&mut list.items);
|
||||
response_list = Some(list);
|
||||
items
|
||||
(items, is_incomplete)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
(Vec::new(), false)
|
||||
};
|
||||
|
||||
let unfiltered_completions_count = completions.len();
|
||||
|
||||
let language_server_adapter = lsp_store
|
||||
.read_with(&mut cx, |lsp_store, _| {
|
||||
lsp_store.language_server_adapter_for_id(server_id)
|
||||
|
@ -2259,11 +2262,17 @@ impl LspCommand for GetCompletions {
|
|||
});
|
||||
})?;
|
||||
|
||||
// If completions were filtered out due to errors that may be transient, mark the result
|
||||
// incomplete so that it is re-queried.
|
||||
if unfiltered_completions_count != completions.len() {
|
||||
is_incomplete = true;
|
||||
}
|
||||
|
||||
language_server_adapter
|
||||
.process_completions(&mut completions)
|
||||
.await;
|
||||
|
||||
Ok(completions
|
||||
let completions = completions
|
||||
.into_iter()
|
||||
.zip(completion_edits)
|
||||
.map(|(mut lsp_completion, mut edit)| {
|
||||
|
@ -2290,7 +2299,12 @@ impl LspCommand for GetCompletions {
|
|||
},
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
.collect();
|
||||
|
||||
Ok(CoreCompletionResponse {
|
||||
completions,
|
||||
is_incomplete,
|
||||
})
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions {
|
||||
|
@ -2332,18 +2346,20 @@ impl LspCommand for GetCompletions {
|
|||
}
|
||||
|
||||
fn response_to_proto(
|
||||
completions: Vec<CoreCompletion>,
|
||||
response: CoreCompletionResponse,
|
||||
_: &mut LspStore,
|
||||
_: PeerId,
|
||||
buffer_version: &clock::Global,
|
||||
_: &mut App,
|
||||
) -> proto::GetCompletionsResponse {
|
||||
proto::GetCompletionsResponse {
|
||||
completions: completions
|
||||
completions: response
|
||||
.completions
|
||||
.iter()
|
||||
.map(LspStore::serialize_completion)
|
||||
.collect(),
|
||||
version: serialize_version(buffer_version),
|
||||
can_reuse: !response.is_incomplete,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2360,11 +2376,16 @@ impl LspCommand for GetCompletions {
|
|||
})?
|
||||
.await?;
|
||||
|
||||
message
|
||||
let completions = message
|
||||
.completions
|
||||
.into_iter()
|
||||
.map(LspStore::deserialize_completion)
|
||||
.collect()
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(CoreCompletionResponse {
|
||||
completions,
|
||||
is_incomplete: !message.can_reuse,
|
||||
})
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::GetCompletions) -> Result<BufferId> {
|
||||
|
|
|
@ -3,8 +3,8 @@ pub mod lsp_ext_command;
|
|||
pub mod rust_analyzer_ext;
|
||||
|
||||
use crate::{
|
||||
CodeAction, Completion, CompletionSource, CoreCompletion, Hover, InlayHint, LspAction,
|
||||
ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
|
||||
CodeAction, Completion, CompletionResponse, CompletionSource, CoreCompletion, Hover, InlayHint,
|
||||
LspAction, ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
|
||||
buffer_store::{BufferStore, BufferStoreEvent},
|
||||
environment::ProjectEnvironment,
|
||||
lsp_command::{self, *},
|
||||
|
@ -998,7 +998,7 @@ impl LocalLspStore {
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
async move {
|
||||
futures::future::join_all(shutdown_futures).await;
|
||||
join_all(shutdown_futures).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5081,7 +5081,7 @@ impl LspStore {
|
|||
position: PointUtf16,
|
||||
context: CompletionContext,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let language_registry = self.languages.clone();
|
||||
|
||||
if let Some((upstream_client, project_id)) = self.upstream_client() {
|
||||
|
@ -5105,11 +5105,17 @@ impl LspStore {
|
|||
});
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let completions = task.await?;
|
||||
let mut result = Vec::new();
|
||||
populate_labels_for_completions(completions, language, lsp_adapter, &mut result)
|
||||
.await;
|
||||
Ok(Some(result))
|
||||
let completion_response = task.await?;
|
||||
let completions = populate_labels_for_completions(
|
||||
completion_response.completions,
|
||||
language,
|
||||
lsp_adapter,
|
||||
)
|
||||
.await;
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
is_incomplete: completion_response.is_incomplete,
|
||||
}])
|
||||
})
|
||||
} else if let Some(local) = self.as_local() {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
@ -5123,7 +5129,7 @@ impl LspStore {
|
|||
)
|
||||
.completions;
|
||||
if !completion_settings.lsp {
|
||||
return Task::ready(Ok(None));
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
|
||||
|
@ -5190,25 +5196,23 @@ impl LspStore {
|
|||
}
|
||||
})?;
|
||||
|
||||
let mut has_completions_returned = false;
|
||||
let mut completions = Vec::new();
|
||||
for (lsp_adapter, task) in tasks {
|
||||
if let Ok(Some(new_completions)) = task.await {
|
||||
has_completions_returned = true;
|
||||
populate_labels_for_completions(
|
||||
new_completions,
|
||||
let futures = tasks.into_iter().map(async |(lsp_adapter, task)| {
|
||||
let completion_response = task.await.ok()??;
|
||||
let completions = populate_labels_for_completions(
|
||||
completion_response.completions,
|
||||
language.clone(),
|
||||
lsp_adapter,
|
||||
&mut completions,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
if has_completions_returned {
|
||||
Ok(Some(completions))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
Some(CompletionResponse {
|
||||
completions,
|
||||
is_incomplete: completion_response.is_incomplete,
|
||||
})
|
||||
});
|
||||
|
||||
let responses: Vec<Option<CompletionResponse>> = join_all(futures).await;
|
||||
|
||||
Ok(responses.into_iter().flatten().collect())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("No upstream client or local language server")))
|
||||
|
@ -9547,8 +9551,7 @@ async fn populate_labels_for_completions(
|
|||
new_completions: Vec<CoreCompletion>,
|
||||
language: Option<Arc<Language>>,
|
||||
lsp_adapter: Option<Arc<CachedLspAdapter>>,
|
||||
completions: &mut Vec<Completion>,
|
||||
) {
|
||||
) -> Vec<Completion> {
|
||||
let lsp_completions = new_completions
|
||||
.iter()
|
||||
.filter_map(|new_completion| {
|
||||
|
@ -9572,6 +9575,7 @@ async fn populate_labels_for_completions(
|
|||
.into_iter()
|
||||
.fuse();
|
||||
|
||||
let mut completions = Vec::new();
|
||||
for completion in new_completions {
|
||||
match completion.source.lsp_completion(true) {
|
||||
Some(lsp_completion) => {
|
||||
|
@ -9612,6 +9616,7 @@ async fn populate_labels_for_completions(
|
|||
}
|
||||
}
|
||||
}
|
||||
completions
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -555,6 +555,23 @@ impl std::fmt::Debug for Completion {
|
|||
}
|
||||
}
|
||||
|
||||
/// Response from a source of completions.
|
||||
pub struct CompletionResponse {
|
||||
pub completions: Vec<Completion>,
|
||||
/// When false, indicates that the list is complete and so does not need to be re-queried if it
|
||||
/// can be filtered instead.
|
||||
pub is_incomplete: bool,
|
||||
}
|
||||
|
||||
/// Response from language server completion request.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct CoreCompletionResponse {
|
||||
pub completions: Vec<CoreCompletion>,
|
||||
/// When false, indicates that the list is complete and so does not need to be re-queried if it
|
||||
/// can be filtered instead.
|
||||
pub is_incomplete: bool,
|
||||
}
|
||||
|
||||
/// A generic completion that can come from different sources.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct CoreCompletion {
|
||||
|
@ -3430,7 +3447,7 @@ impl Project {
|
|||
position: T,
|
||||
context: CompletionContext,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<Completion>>>> {
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.completions(buffer, position, context, cx)
|
||||
|
|
|
@ -3014,7 +3014,12 @@ async fn test_completions_with_text_edit(cx: &mut gpui::TestAppContext) {
|
|||
.next()
|
||||
.await;
|
||||
|
||||
let completions = completions.await.unwrap().unwrap();
|
||||
let completions = completions
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.flat_map(|response| response.completions)
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
assert_eq!(completions.len(), 1);
|
||||
|
@ -3097,7 +3102,12 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
|
|||
.next()
|
||||
.await;
|
||||
|
||||
let completions = completions.await.unwrap().unwrap();
|
||||
let completions = completions
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.flat_map(|response| response.completions)
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
assert_eq!(completions.len(), 1);
|
||||
|
@ -3139,7 +3149,12 @@ async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
|
|||
.next()
|
||||
.await;
|
||||
|
||||
let completions = completions.await.unwrap().unwrap();
|
||||
let completions = completions
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.flat_map(|response| response.completions)
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
assert_eq!(completions.len(), 1);
|
||||
|
@ -3210,7 +3225,12 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
|
|||
})
|
||||
.next()
|
||||
.await;
|
||||
let completions = completions.await.unwrap().unwrap();
|
||||
let completions = completions
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.flat_map(|response| response.completions)
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
assert_eq!(completions.len(), 1);
|
||||
assert_eq!(completions[0].new_text, "fullyQualifiedName");
|
||||
|
@ -3237,7 +3257,12 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
|
|||
})
|
||||
.next()
|
||||
.await;
|
||||
let completions = completions.await.unwrap().unwrap();
|
||||
let completions = completions
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.flat_map(|response| response.completions)
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
||||
assert_eq!(completions.len(), 1);
|
||||
assert_eq!(completions[0].new_text, "component");
|
||||
|
@ -3305,7 +3330,12 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
|
|||
})
|
||||
.next()
|
||||
.await;
|
||||
let completions = completions.await.unwrap().unwrap();
|
||||
let completions = completions
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.flat_map(|response| response.completions)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(completions.len(), 1);
|
||||
assert_eq!(completions[0].new_text, "fully\nQualified\nName");
|
||||
}
|
||||
|
|
|
@ -195,6 +195,8 @@ message LspExtGoToParentModuleResponse {
|
|||
message GetCompletionsResponse {
|
||||
repeated Completion completions = 1;
|
||||
repeated VectorClockEntry version = 2;
|
||||
// `!is_complete`, inverted for a default of `is_complete = true`
|
||||
bool can_reuse = 3;
|
||||
}
|
||||
|
||||
message ApplyCompletionAdditionalEdits {
|
||||
|
|
|
@ -513,8 +513,8 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
|
|||
|
||||
assert_eq!(
|
||||
result
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.flat_map(|response| response.completions)
|
||||
.map(|c| c.label.text)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["boop".to_string()]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue