Detailed changes
@@ -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, |_| {});