Follow-up of https://github.com/zed-industries/zed/pull/26410 * Extract word completions into their own, `editor::ShowWordCompletions` action so those could be triggered independently of completions * Assign `ctrl-shift-space` binding to this new action * Still keep words returned along the completions as in the original PR, but: * Tone down regular completions' fallback logic, skip words when the language server responds with empty list of completions, but keep on adding words if nothing or an error were returned instead * Adjust the defaults to wait for LSP completions infinitely * Skip "words" with digits such as `0_usize` or `2.f32` from completion items, unless a completion query has digits in it Release Notes: - N/A
344 lines
15 KiB
Rust
344 lines
15 KiB
Rust
use crate::context_editor::ContextEditor;
|
|
use anyhow::Result;
|
|
pub use assistant_slash_command::SlashCommand;
|
|
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
|
|
use editor::{CompletionProvider, Editor};
|
|
use fuzzy::{match_strings, StringMatchCandidate};
|
|
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
|
use language::{Anchor, Buffer, ToPoint};
|
|
use parking_lot::Mutex;
|
|
use project::{lsp_store::CompletionDocumentation, CompletionIntent, CompletionSource};
|
|
use rope::Point;
|
|
use std::{
|
|
cell::RefCell,
|
|
ops::Range,
|
|
rc::Rc,
|
|
sync::{
|
|
atomic::{AtomicBool, Ordering::SeqCst},
|
|
Arc,
|
|
},
|
|
};
|
|
use workspace::Workspace;
|
|
|
|
pub struct SlashCommandCompletionProvider {
|
|
cancel_flag: Mutex<Arc<AtomicBool>>,
|
|
slash_commands: Arc<SlashCommandWorkingSet>,
|
|
editor: Option<WeakEntity<ContextEditor>>,
|
|
workspace: Option<WeakEntity<Workspace>>,
|
|
}
|
|
|
|
impl SlashCommandCompletionProvider {
|
|
pub fn new(
|
|
slash_commands: Arc<SlashCommandWorkingSet>,
|
|
editor: Option<WeakEntity<ContextEditor>>,
|
|
workspace: Option<WeakEntity<Workspace>>,
|
|
) -> Self {
|
|
Self {
|
|
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
|
slash_commands,
|
|
editor,
|
|
workspace,
|
|
}
|
|
}
|
|
|
|
fn complete_command_name(
|
|
&self,
|
|
command_name: &str,
|
|
command_range: Range<Anchor>,
|
|
name_range: Range<Anchor>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
|
let slash_commands = self.slash_commands.clone();
|
|
let candidates = slash_commands
|
|
.command_names(cx)
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(ix, def)| StringMatchCandidate::new(ix, &def))
|
|
.collect::<Vec<_>>();
|
|
let command_name = command_name.to_string();
|
|
let editor = self.editor.clone();
|
|
let workspace = self.workspace.clone();
|
|
window.spawn(cx, |mut cx| async move {
|
|
let matches = match_strings(
|
|
&candidates,
|
|
&command_name,
|
|
true,
|
|
usize::MAX,
|
|
&Default::default(),
|
|
cx.background_executor().clone(),
|
|
)
|
|
.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 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| {
|
|
if !requires_argument
|
|
&& (!accepts_arguments || intent.is_complete())
|
|
{
|
|
editor
|
|
.update(cx, |editor, cx| {
|
|
editor.run_command(
|
|
command_range.clone(),
|
|
&command_name,
|
|
&[],
|
|
true,
|
|
workspace.clone(),
|
|
window,
|
|
cx,
|
|
);
|
|
})
|
|
.ok();
|
|
false
|
|
} else {
|
|
requires_argument || accepts_arguments
|
|
}
|
|
},
|
|
) as Arc<_>
|
|
});
|
|
Some(project::Completion {
|
|
old_range: name_range.clone(),
|
|
documentation: Some(CompletionDocumentation::SingleLine(
|
|
command.description().into(),
|
|
)),
|
|
new_text,
|
|
label: command.label(cx),
|
|
confirm,
|
|
source: CompletionSource::Custom,
|
|
})
|
|
})
|
|
.collect(),
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
fn complete_command_argument(
|
|
&self,
|
|
command_name: &str,
|
|
arguments: &[String],
|
|
command_range: Range<Anchor>,
|
|
argument_range: Range<Anchor>,
|
|
last_argument_range: Range<Anchor>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
|
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
|
let mut flag = self.cancel_flag.lock();
|
|
flag.store(true, SeqCst);
|
|
*flag = new_cancel_flag.clone();
|
|
if let Some(command) = self.slash_commands.command(command_name, cx) {
|
|
let completions = command.complete_argument(
|
|
arguments,
|
|
new_cancel_flag.clone(),
|
|
self.workspace.clone(),
|
|
window,
|
|
cx,
|
|
);
|
|
let command_name: Arc<str> = command_name.into();
|
|
let editor = self.editor.clone();
|
|
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 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()
|
|
|| intent.is_complete()
|
|
{
|
|
editor
|
|
.update(cx, |editor, cx| {
|
|
editor.run_command(
|
|
command_range.clone(),
|
|
&command_name,
|
|
&completed_arguments,
|
|
true,
|
|
workspace.clone(),
|
|
window,
|
|
cx,
|
|
);
|
|
})
|
|
.ok();
|
|
false
|
|
} else {
|
|
!new_argument.after_completion.run()
|
|
}
|
|
}
|
|
}) as Arc<_>
|
|
});
|
|
|
|
let mut new_text = new_argument.new_text.clone();
|
|
if new_argument.after_completion == AfterCompletion::Continue {
|
|
new_text.push(' ');
|
|
}
|
|
|
|
project::Completion {
|
|
old_range: if new_argument.replace_previous_arguments {
|
|
argument_range.clone()
|
|
} else {
|
|
last_argument_range.clone()
|
|
},
|
|
label: new_argument.label,
|
|
new_text,
|
|
documentation: None,
|
|
confirm,
|
|
source: CompletionSource::Custom,
|
|
}
|
|
})
|
|
.collect(),
|
|
))
|
|
})
|
|
} else {
|
|
Task::ready(Ok(Some(Vec::new())))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CompletionProvider for SlashCommandCompletionProvider {
|
|
fn completions(
|
|
&self,
|
|
buffer: &Entity<Buffer>,
|
|
buffer_position: Anchor,
|
|
_: editor::CompletionContext,
|
|
window: &mut Window,
|
|
cx: &mut Context<Editor>,
|
|
) -> Task<Result<Option<Vec<project::Completion>>>> {
|
|
let Some((name, arguments, command_range, last_argument_range)) =
|
|
buffer.update(cx, |buffer, _cx| {
|
|
let position = buffer_position.to_point(buffer);
|
|
let line_start = Point::new(position.row, 0);
|
|
let mut lines = buffer.text_for_range(line_start..position).lines();
|
|
let line = lines.next()?;
|
|
let call = SlashCommandLine::parse(line)?;
|
|
|
|
let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
|
|
let command_range_end = Point::new(
|
|
position.row,
|
|
call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
|
|
);
|
|
let command_range = buffer.anchor_after(command_range_start)
|
|
..buffer.anchor_after(command_range_end);
|
|
|
|
let name = line[call.name.clone()].to_string();
|
|
let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
|
|
{
|
|
let last_arg_start =
|
|
buffer.anchor_after(Point::new(position.row, argument.start as u32));
|
|
let first_arg_start = call.arguments.first().expect("we have the last element");
|
|
let first_arg_start =
|
|
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
|
|
let arguments = call
|
|
.arguments
|
|
.iter()
|
|
.filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
|
|
.collect::<Vec<_>>();
|
|
let argument_range = first_arg_start..buffer_position;
|
|
(
|
|
Some((arguments, argument_range)),
|
|
last_arg_start..buffer_position,
|
|
)
|
|
} else {
|
|
let start =
|
|
buffer.anchor_after(Point::new(position.row, call.name.start as u32));
|
|
(None, start..buffer_position)
|
|
};
|
|
|
|
Some((name, arguments, command_range, last_argument_range))
|
|
})
|
|
else {
|
|
return Task::ready(Ok(Some(Vec::new())));
|
|
};
|
|
|
|
if let Some((arguments, argument_range)) = arguments {
|
|
self.complete_command_argument(
|
|
&name,
|
|
&arguments,
|
|
command_range,
|
|
argument_range,
|
|
last_argument_range,
|
|
window,
|
|
cx,
|
|
)
|
|
} else {
|
|
self.complete_command_name(&name, command_range, last_argument_range, window, cx)
|
|
}
|
|
}
|
|
|
|
fn resolve_completions(
|
|
&self,
|
|
_: Entity<Buffer>,
|
|
_: Vec<usize>,
|
|
_: Rc<RefCell<Box<[project::Completion]>>>,
|
|
_: &mut Context<Editor>,
|
|
) -> Task<Result<bool>> {
|
|
Task::ready(Ok(true))
|
|
}
|
|
|
|
fn is_completion_trigger(
|
|
&self,
|
|
buffer: &Entity<Buffer>,
|
|
position: language::Anchor,
|
|
_text: &str,
|
|
_trigger_in_words: bool,
|
|
cx: &mut Context<Editor>,
|
|
) -> bool {
|
|
let buffer = buffer.read(cx);
|
|
let position = position.to_point(buffer);
|
|
let line_start = Point::new(position.row, 0);
|
|
let mut lines = buffer.text_for_range(line_start..position).lines();
|
|
if let Some(line) = lines.next() {
|
|
SlashCommandLine::parse(line).is_some()
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn sort_completions(&self) -> bool {
|
|
false
|
|
}
|
|
}
|