Run commands if completion items require so (#44008)
Abide the LSP better and actually run commands if completion items request those: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItem ``` /** * An optional command that is executed *after* inserting this completion. * *Note* that additional modifications to the current document should be * described with the additionalTextEdits-property. */ command?: [Command](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#command); ``` Release Notes: - N/A
This commit is contained in:
@@ -18,14 +18,14 @@ use gpui::{
|
||||
use language::{Anchor, Buffer, CharScopeContext, CodeLabel, TextBufferSnapshot, ToOffset};
|
||||
use menu::{Confirm, SelectNext, SelectPrevious};
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionResponse,
|
||||
CompletionDisplayOptions, CompletionResponse,
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session},
|
||||
lsp_store::CompletionDocumentation,
|
||||
search_history::{SearchHistory, SearchHistoryCursor},
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::fmt::Write;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
|
||||
use std::{ops::Range, rc::Rc, usize};
|
||||
use theme::{Theme, ThemeSettings};
|
||||
use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
@@ -553,17 +553,6 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_completion_index: usize,
|
||||
_push_to_history: bool,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<anyhow::Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
||||
@@ -146,8 +146,8 @@ use persistence::DB;
|
||||
use project::{
|
||||
BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
|
||||
CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId,
|
||||
InvalidationStrategy, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem,
|
||||
ProjectPath, ProjectTransaction, TaskSourceKind,
|
||||
InvalidationStrategy, Location, LocationLink, LspAction, PrepareRenameResponse, Project,
|
||||
ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind,
|
||||
debugger::{
|
||||
breakpoint_store::{
|
||||
Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
|
||||
@@ -6151,9 +6151,43 @@ impl Editor {
|
||||
}
|
||||
|
||||
let provider = self.completion_provider.as_ref()?;
|
||||
|
||||
let lsp_store = self.project().map(|project| project.read(cx).lsp_store());
|
||||
let command = lsp_store.as_ref().and_then(|lsp_store| {
|
||||
let CompletionSource::Lsp {
|
||||
lsp_completion,
|
||||
server_id,
|
||||
..
|
||||
} = &completion.source
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let lsp_command = lsp_completion.command.as_ref()?;
|
||||
let available_commands = lsp_store
|
||||
.read(cx)
|
||||
.lsp_server_capabilities
|
||||
.get(server_id)
|
||||
.and_then(|server_capabilities| {
|
||||
server_capabilities
|
||||
.execute_command_provider
|
||||
.as_ref()
|
||||
.map(|options| options.commands.as_slice())
|
||||
})?;
|
||||
if available_commands.contains(&lsp_command.command) {
|
||||
Some(CodeAction {
|
||||
server_id: *server_id,
|
||||
range: language::Anchor::MIN..language::Anchor::MIN,
|
||||
lsp_action: LspAction::Command(lsp_command.clone()),
|
||||
resolved: false,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
drop(completion);
|
||||
let apply_edits = provider.apply_additional_edits_for_completion(
|
||||
buffer_handle,
|
||||
buffer_handle.clone(),
|
||||
completions_menu.completions.clone(),
|
||||
candidate_id,
|
||||
true,
|
||||
@@ -6167,8 +6201,29 @@ impl Editor {
|
||||
self.show_signature_help(&ShowSignatureHelp, window, cx);
|
||||
}
|
||||
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
Some(cx.spawn_in(window, async move |editor, cx| {
|
||||
apply_edits.await?;
|
||||
|
||||
if let Some((lsp_store, command)) = lsp_store.zip(command) {
|
||||
let title = command.lsp_action.title().to_owned();
|
||||
let project_transaction = lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
lsp_store.apply_code_action(buffer_handle, command, false, cx)
|
||||
})?
|
||||
.await
|
||||
.context("applying post-completion command")?;
|
||||
if let Some(workspace) = editor.read_with(cx, |editor, _| editor.workspace())? {
|
||||
Self::open_project_transaction(
|
||||
&editor,
|
||||
workspace.downgrade(),
|
||||
project_transaction,
|
||||
title,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -14755,6 +14755,180 @@ async fn test_completion(cx: &mut TestAppContext) {
|
||||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/a"),
|
||||
json!({
|
||||
"main.rs": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(rust_lang());
|
||||
let command_calls = Arc::new(AtomicUsize::new(0));
|
||||
let registered_command = "_the/command";
|
||||
|
||||
let closure_command_calls = command_calls.clone();
|
||||
let mut fake_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
execute_command_provider: Some(lsp::ExecuteCommandOptions {
|
||||
commands: vec![registered_command.to_owned()],
|
||||
..lsp::ExecuteCommandOptions::default()
|
||||
}),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
initializer: Some(Box::new(move |fake_server| {
|
||||
fake_server.set_request_handler::<lsp::request::Completion, _, _>(
|
||||
move |params, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "registered_command".to_owned(),
|
||||
text_edit: gen_text_edit(¶ms, ""),
|
||||
command: Some(lsp::Command {
|
||||
title: registered_command.to_owned(),
|
||||
command: "_the/command".to_owned(),
|
||||
arguments: Some(vec![serde_json::Value::Bool(true)]),
|
||||
}),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "unregistered_command".to_owned(),
|
||||
text_edit: gen_text_edit(¶ms, ""),
|
||||
command: Some(lsp::Command {
|
||||
title: "????????????".to_owned(),
|
||||
command: "????????????".to_owned(),
|
||||
arguments: Some(vec![serde_json::Value::Null]),
|
||||
}),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
])))
|
||||
},
|
||||
);
|
||||
fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
|
||||
let command_calls = closure_command_calls.clone();
|
||||
move |params, _| {
|
||||
assert_eq!(params.command, registered_command);
|
||||
let command_calls = command_calls.clone();
|
||||
async move {
|
||||
command_calls.fetch_add(1, atomic::Ordering::Release);
|
||||
Ok(Some(json!(null)))
|
||||
}
|
||||
}
|
||||
});
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let editor = workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
workspace.open_abs_path(
|
||||
PathBuf::from(path!("/a/main.rs")),
|
||||
OpenOptions::default(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let _fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
cx.focus_self(window);
|
||||
editor.move_to_end(&MoveToEnd, window, cx);
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
editor.update(cx, |editor, _| {
|
||||
assert!(editor.context_menu_visible());
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
let completion_labels = menu
|
||||
.completions
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|c| c.label.text.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
completion_labels,
|
||||
&["registered_command", "unregistered_command",],
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
command_calls.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"For completion with a registered command, Zed should send a command execution request",
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
cx.focus_self(window);
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
editor.update(cx, |editor, _| {
|
||||
assert!(editor.context_menu_visible());
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
let completion_labels = menu
|
||||
.completions
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|c| c.label.text.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
completion_labels,
|
||||
&["registered_command", "unregistered_command",],
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.context_menu_next(&Default::default(), window, cx);
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
command_calls.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"For completion with an unregistered command, Zed should not send a command execution request",
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_reuse(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
Reference in New Issue
Block a user