Support LSP command executions and showDocument requests (#63607)

Closes https://github.com/zed-industries/zed/issues/13756
Closes https://github.com/zed-industries/zed/issues/61572

Last window client capability we were missing!
<img width="1557" height="159" alt="image"
src="https://github.com/user-attachments/assets/2cca5d92-a28e-4262-8f55-89f582782bbc"
/>

Two features are connected to each other, as most of the commands result
in opening a document:


https://github.com/user-attachments/assets/661aae88-7af9-4173-9f6a-2fd11dbec675


Release Notes:

- Added support for LSP command executions and showDocument requests
This commit is contained in:
Kirill Bulatov authored and GitHub committed 2026-09-09 13:47:28 +00:00
1 parent 5697636cd5
commit 66a1d8d7cb
23 files changed
+2019 -29

No files matched your search

Generated
+24
View File
@@ -10417,6 +10417,29 @@ dependencies = [
"url",
]
[[package]]
name = "lsp_command_selector"
version = "0.1.0"
dependencies = [
"anyhow",
"editor",
"futures 0.3.32",
"fuzzy",
"gpui",
"language",
"log",
"lsp",
"menu",
"picker",
"project",
"serde_json",
"settings",
"ui",
"util",
"workspace",
"zlog",
]
[[package]]
name = "lsp_locations"
version = "0.1.0"
@@ -22806,6 +22829,7 @@ dependencies = [
"languages",
"line_ending_selector",
"log",
"lsp_command_selector",
"lsp_locations",
"markdown",
"markdown_preview",
+2
View File
@@ -132,6 +132,7 @@ members = [
"crates/llama_cpp",
"crates/lmstudio",
"crates/lsp",
"crates/lsp_command_selector",
"crates/lsp_locations",
"crates/markdown",
"crates/markdown_preview",
@@ -399,6 +400,7 @@ livekit_client = { path = "crates/livekit_client" }
llama_cpp = { path = "crates/llama_cpp" }
lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
lsp_command_selector = { path = "crates/lsp_command_selector" }
lsp_locations = { path = "crates/lsp_locations" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
+6
View File
@@ -1222,6 +1222,12 @@
// Editor bindings.
},
},
{
"context": "LspCommandSelector > Picker > Editor",
"bindings": {
"tab": "lsp_command_selector::ToggleArgumentsFocus",
},
},
{
"context": "ChannelModal > Picker > Editor",
"bindings": {
+7
View File
@@ -1275,6 +1275,13 @@
// Editor bindings.
},
},
{
"context": "LspCommandSelector > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"tab": "lsp_command_selector::ToggleArgumentsFocus",
},
},
{
"context": "ChannelModal > Picker > Editor",
"use_key_equivalents": true,
+7
View File
@@ -1230,6 +1230,13 @@
// Editor bindings.
},
},
{
"context": "LspCommandSelector > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"tab": "lsp_command_selector::ToggleArgumentsFocus",
},
},
{
"context": "ChannelModal > Picker > Editor",
"use_key_equivalents": true,
+1
View File
@@ -401,6 +401,7 @@ impl Server {
forward_mutating_project_request::<proto::ResolveCompletionDocumentation>,
)
.add_request_handler(forward_mutating_project_request::<proto::ApplyCodeAction>)
.add_request_handler(forward_mutating_project_request::<proto::ExecuteLspCommand>)
.add_request_handler(forward_mutating_project_request::<proto::PrepareRename>)
.add_request_handler(forward_mutating_project_request::<proto::PerformRename>)
.add_request_handler(forward_mutating_project_request::<proto::ReloadBuffers>)
+14 -1
View File
@@ -365,7 +365,7 @@ pub fn init(cx: &mut App) {
workspace::register_serializable_item::<Editor>(cx);
cx.observe_new(
|workspace: &mut Workspace, _: Option<&mut Window>, _cx: &mut Context<Workspace>| {
|workspace: &mut Workspace, window: Option<&mut Window>, cx: &mut Context<Workspace>| {
workspace.register_action(Editor::new_file);
workspace.register_action(Editor::new_file_split);
workspace.register_action(Editor::new_file_vertical);
@@ -373,6 +373,19 @@ pub fn init(cx: &mut App) {
workspace.register_action(Editor::cancel_language_server_work);
workspace.register_action(Editor::toggle_focus);
workspace.register_action(Editor::view_bookmarks);
if let Some(window) = window {
cx.subscribe_in(
workspace.project(),
window,
|workspace, _, event, window, cx| {
if let project::Event::LanguageServerShowDocument(request) = event {
items::handle_lsp_show_document(workspace, request, window, cx)
.detach();
}
},
)
.detach();
}
},
)
.detach();
+161
View File
@@ -46051,3 +46051,164 @@ async fn test_scroll_range_hold_freezes_before_first_settled_frame(cx: &mut Test
);
});
}
#[gpui::test]
async fn test_lsp_show_document(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
let target_path = EditorLspTestContext::root_path()
.join("dir")
.join("target.rs");
let fs = cx.update_workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
fs.as_fake()
.insert_file(&target_path, b"fn target() {}".to_vec())
.await;
let response = cx
.lsp
.server
.request::<lsp::request::ShowDocument>(
lsp::ShowDocumentParams {
uri: lsp::Uri::from_file_path(&target_path).unwrap(),
external: None,
take_focus: Some(true),
selection: Some(lsp::Range::new(
lsp::Position::new(0, 3),
lsp::Position::new(0, 9),
)),
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await
.into_response()
.expect("show document request should not error");
assert_eq!(response, lsp::ShowDocumentResult { success: true });
cx.run_until_parked();
cx.update_workspace(|workspace, _, cx| {
let editor = workspace
.active_item_as::<Editor>(cx)
.expect("an editor should be opened for the shown document");
editor.update(cx, |editor, cx| {
let path = editor
.buffer()
.read(cx)
.as_singleton()
.expect("a singleton buffer should be opened")
.read(cx)
.file()
.expect("the opened buffer should have a file")
.path()
.clone();
assert_eq!(path.as_ref(), rel_path("dir/target.rs"));
assert_eq!(
editor
.selections
.ranges::<Point>(&editor.display_snapshot(cx)),
vec![Point::new(0, 3)..Point::new(0, 9)]
);
});
});
}
#[gpui::test]
async fn test_lsp_show_document_external(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
let initial_item_id = cx
.update_workspace(|workspace, _, cx| workspace.active_item(cx).map(|item| item.item_id()));
let response = cx
.lsp
.server
.request::<lsp::request::ShowDocument>(
lsp::ShowDocumentParams {
uri: "https://zed.dev/docs".parse::<lsp::Uri>().unwrap(),
external: Some(true),
take_focus: None,
selection: None,
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await
.into_response()
.expect("show document request should not error");
assert_eq!(response, lsp::ShowDocumentResult { success: true });
assert_eq!(cx.opened_url(), Some("https://zed.dev/docs".to_string()));
cx.run_until_parked();
cx.update_workspace(|workspace, _, cx| {
assert_eq!(
workspace.active_item(cx).map(|item| item.item_id()),
initial_item_id,
"external documents should not open any workspace items"
);
});
}
#[gpui::test]
async fn test_lsp_show_document_without_take_focus(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
let target_path = EditorLspTestContext::root_path()
.join("dir")
.join("target.rs");
let fs = cx.update_workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
fs.as_fake()
.insert_file(&target_path, b"fn target() {}".to_vec())
.await;
let initial_editor = cx.editor.clone();
let response = cx
.lsp
.server
.request::<lsp::request::ShowDocument>(
lsp::ShowDocumentParams {
uri: lsp::Uri::from_file_path(&target_path).unwrap(),
external: None,
take_focus: None,
selection: None,
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await
.into_response()
.expect("show document request should not error");
assert_eq!(response, lsp::ShowDocumentResult { success: true });
cx.run_until_parked();
cx.update_workspace(|workspace, _, cx| {
let opened_editor = workspace
.active_item_as::<Editor>(cx)
.expect("an editor should be opened for the shown document");
assert_ne!(
opened_editor.entity_id(),
initial_editor.entity_id(),
"the shown document should become the active item"
);
});
}
#[gpui::test]
async fn test_lsp_show_document_unsupported_uri(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
let response = cx
.lsp
.server
.request::<lsp::request::ShowDocument>(
lsp::ShowDocumentParams {
uri: "untitled:some-document".parse::<lsp::Uri>().unwrap(),
external: None,
take_focus: None,
selection: None,
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await
.into_response()
.expect("show document request should not error");
assert_eq!(response, lsp::ShowDocumentResult { success: false });
}
+74 -4
View File
@@ -26,8 +26,11 @@ use language::{
use lsp::DiagnosticSeverity;
use multi_buffer::{BufferOffset, MultiBufferOffset, MultiBufferRow, PathKey};
use project::{
File, Project, ProjectItem as _, ProjectPath, git_store::GitStore, lsp_store::FormatTrigger,
project_settings::ProjectSettings, search::SearchQuery,
File, Project, ProjectItem as _, ProjectPath,
git_store::GitStore,
lsp_store::{FormatTrigger, LanguageServerShowDocumentRequest},
project_settings::ProjectSettings,
search::SearchQuery,
};
use rope::TextSummary;
use rpc::proto::{self, update_view};
@@ -43,10 +46,15 @@ use std::{
};
use text::{BufferId, BufferSnapshot, OffsetRangeExt, Selection, ToPoint as _};
use ui::{IconDecorationKind, prelude::*};
use util::{ResultExt, TryFutureExt, debug_panic, paths::PathExt, rel_path::RelPath};
use util::{
ResultExt, TryFutureExt, debug_panic,
paths::{PathExt, UrlExt as _},
rel_path::RelPath,
};
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
use workspace::{
CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
CollaboratorId, ItemId, ItemNavHistory, OpenOptions, OpenVisible, ToolbarItemLocation, ViewId,
Workspace, WorkspaceId,
invalid_item_view::InvalidItemView,
item::{FollowableItem, Item, ItemBufferKind, ItemEvent, ProjectItem, SaveOptions},
searchable::{
@@ -2504,6 +2512,68 @@ fn compute_modified_ranges(
merged
}
pub(crate) fn handle_lsp_show_document(
workspace: &mut Workspace,
request: &LanguageServerShowDocumentRequest,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<()> {
let request = request.clone();
if request.external {
cx.open_url(request.uri.as_str());
request.respond(true);
return Task::ready(());
}
let Ok(abs_path) = request.uri.to_file_path_ext(workspace.path_style(cx)) else {
log::error!(
"language server requested to show document with unsupported uri {}",
request.uri.as_str()
);
request.respond(false);
return Task::ready(());
};
let open_task = workspace.open_abs_path(
abs_path,
OpenOptions {
visible: Some(OpenVisible::None),
focus: Some(request.take_focus),
..OpenOptions::default()
},
window,
cx,
);
cx.spawn_in(window, async move |_, cx| {
let success = match open_task.await {
Ok(item) => match item.downcast::<Editor>().zip(request.selection) {
Some((editor, selection)) => editor
.update_in(cx, |editor, window, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let range = language::range_from_lsp(selection);
let start = snapshot.point_utf16_to_offset(
snapshot.clip_point_utf16(range.start, Bias::Left),
);
let end = snapshot.point_utf16_to_offset(
snapshot.clip_point_utf16(range.end, Bias::Left),
);
editor.change_selections(
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|selections| selections.select_ranges([start..end]),
);
})
.is_ok(),
None => true,
},
Err(error) => {
log::error!("failed to show document for a language server: {error:#}");
false
}
};
request.respond(success);
})
}
#[cfg(test)]
mod tests {
use crate::editor_tests::init_test;
+1 -1
View File
@@ -1141,7 +1141,7 @@ impl LanguageServer {
additional_properties_support: Some(true),
}),
}),
..WindowClientCapabilities::default()
show_document: Some(ShowDocumentClientCapabilities { support: true }),
}),
},
trace: None,
+40
View File
@@ -0,0 +1,40 @@
[package]
name = "lsp_command_selector"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/lsp_command_selector.rs"
doctest = false
[dependencies]
editor.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
picker.workspace = true
project.workspace = true
serde_json.workspace = true
ui.workspace = true
workspace.workspace = true
[dev-dependencies]
anyhow.workspace = true
editor = { workspace = true, features = ["test-support"] }
futures.workspace = true
menu.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
util.workspace = true
workspace = { workspace = true, features = ["test-support"] }
zlog.workspace = true
+1
View File
@@ -0,0 +1 @@
../../LICENSE-GPL
File diff suppressed because it is too large. Load diff
+150 -20
View File
@@ -1194,11 +1194,11 @@ impl LocalLspStore {
language_server
.on_request::<lsp::request::ShowMessageRequest, _, _>({
let this = lsp_store.clone();
let lsp_store = lsp_store.clone();
let name = name.to_string();
let adapter = adapter.clone();
move |params, cx| {
let this = this.clone();
let lsp_store = lsp_store.clone();
let name = name.to_string();
let adapter = adapter.clone();
let mut cx = cx.clone();
@@ -1219,29 +1219,49 @@ impl LocalLspStore {
tx,
);
let did_update = this
.update(&mut cx, |_, cx| {
cx.emit(LspStoreEvent::LanguageServerPrompt(request));
})
.is_ok();
if did_update {
let response = rx.recv().await.ok();
if let Some(ref selected_action) = response {
let context = language::PromptResponseContext {
message,
selected_action: selected_action.clone(),
};
adapter.process_prompt_response(&context, &mut cx)
}
Ok(response)
} else {
Ok(None)
lsp_store.update(&mut cx, |_, cx| {
cx.emit(LspStoreEvent::LanguageServerPrompt(request));
})?;
let response = rx.recv().await.ok();
if let Some(ref selected_action) = response {
let context = language::PromptResponseContext {
message,
selected_action: selected_action.clone(),
};
adapter.process_prompt_response(&context, &mut cx)
}
Ok(response)
}
}
})
.detach();
language_server
.on_request::<lsp::request::ShowDocument, _, _>({
let lsp_store = lsp_store.clone();
move |params, cx| {
let lsp_store = lsp_store.clone();
let mut cx = cx.clone();
async move {
let (tx, rx) = async_channel::bounded(1);
let request = LanguageServerShowDocumentRequest {
uri: params.uri,
external: params.external.unwrap_or(false),
take_focus: params.take_focus.unwrap_or(false),
selection: params.selection,
response_channel: tx,
};
lsp_store.update(&mut cx, |_, cx| {
cx.emit(LspStoreEvent::LanguageServerShowDocument(request));
})?;
let success = rx.recv().await.unwrap_or(false);
Ok(lsp::ShowDocumentResult { success })
}
}
})
.detach();
language_server
.on_notification::<lsp::notification::ShowMessage, _>({
let this = lsp_store.clone();
@@ -4606,6 +4626,7 @@ pub enum LspStoreEvent {
new_language: Option<Arc<Language>>,
},
Notification(String),
LanguageServerShowDocument(LanguageServerShowDocumentRequest),
RefreshInlayHints {
server_id: LanguageServerId,
},
@@ -4757,6 +4778,7 @@ impl LspStore {
});
client.add_entity_request_handler(Self::handle_lsp_command::<LinkedEditingRange>);
client.add_entity_request_handler(Self::handle_execute_lsp_command);
client.add_entity_request_handler(Self::handle_lsp_ext_cancel_flycheck);
client.add_entity_request_handler(Self::handle_lsp_ext_run_flycheck);
client.add_entity_request_handler(Self::handle_lsp_ext_clear_flycheck);
@@ -6555,6 +6577,67 @@ impl LspStore {
}
}
pub fn execute_lsp_command(
&self,
server_id: LanguageServerId,
command: String,
arguments: Vec<serde_json::Value>,
cx: &mut Context<Self>,
) -> Task<Result<Option<serde_json::Value>>> {
if let Some((upstream_client, project_id)) = self.upstream_client() {
let request = upstream_client.request(proto::ExecuteLspCommand {
project_id,
language_server_id: server_id.to_proto(),
command,
arguments: arguments
.iter()
.map(|argument| argument.to_string())
.collect(),
});
cx.background_spawn(async move {
let response = request.await?;
response
.result
.map(|result| serde_json::from_str(&result))
.transpose()
.context("deserializing executeCommand result")
})
} else if self.mode.is_local() {
let Some(server) = self.language_server_for_id(server_id) else {
return Task::ready(Err(anyhow!("language server {server_id} not found")));
};
let available_commands = server
.capabilities()
.execute_command_provider
.as_ref()
.map(|options| options.commands.clone())
.unwrap_or_default();
if !available_commands.contains(&command) {
return Task::ready(Err(anyhow!(
"command {command} is not advertised by the language server"
)));
}
let request_timeout = ProjectSettings::get_global(cx)
.global_lsp_settings
.get_request_timeout();
cx.background_spawn(async move {
server
.request::<lsp::request::ExecuteCommand>(
lsp::ExecuteCommandParams {
command,
arguments,
..lsp::ExecuteCommandParams::default()
},
request_timeout,
)
.await
.into_response()
})
} else {
Task::ready(Err(anyhow!("no upstream client and not local")))
}
}
pub fn resolve_code_action(
&self,
buffer: &Entity<Buffer>,
@@ -11403,6 +11486,29 @@ impl LspStore {
Ok(proto::Ack {})
}
async fn handle_execute_lsp_command(
lsp_store: Entity<Self>,
envelope: TypedEnvelope<proto::ExecuteLspCommand>,
mut cx: AsyncApp,
) -> Result<proto::ExecuteLspCommandResponse> {
let server_id = LanguageServerId::from_proto(envelope.payload.language_server_id);
let arguments = envelope
.payload
.arguments
.iter()
.map(|argument| serde_json::from_str(argument))
.collect::<Result<Vec<serde_json::Value>, _>>()
.context("deserializing executeCommand arguments")?;
let result = lsp_store
.update(&mut cx, |lsp_store, cx| {
lsp_store.execute_lsp_command(server_id, envelope.payload.command, arguments, cx)
})
.await?;
Ok(proto::ExecuteLspCommandResponse {
result: result.map(|result| result.to_string()),
})
}
async fn handle_lsp_ext_run_flycheck(
lsp_store: Entity<Self>,
envelope: TypedEnvelope<proto::LspExtRunFlycheck>,
@@ -15804,6 +15910,30 @@ impl PartialEq for LanguageServerPromptRequest {
}
}
#[derive(Clone, Debug)]
pub struct LanguageServerShowDocumentRequest {
pub uri: lsp::Uri,
pub external: bool,
pub take_focus: bool,
pub selection: Option<lsp::Range>,
pub(crate) response_channel: async_channel::Sender<bool>,
}
impl LanguageServerShowDocumentRequest {
pub fn respond(self, success: bool) {
self.response_channel.try_send(success).ok();
}
}
impl PartialEq for LanguageServerShowDocumentRequest {
fn eq(&self, other: &Self) -> bool {
self.uri == other.uri
&& self.external == other.external
&& self.take_focus == other.take_focus
&& self.selection == other.selection
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum LanguageServerLogType {
Log(MessageType),
+52 -2
View File
@@ -167,8 +167,9 @@ pub use buffer_store::ProjectTransaction;
pub use lsp_command::{CallHierarchyItem, IncomingCall, OutgoingCall};
pub use lsp_store::{
DiagnosticSummary, InvalidationStrategy, LanguageServerLogType, LanguageServerProgress,
LanguageServerPromptRequest, LanguageServerStatus, LanguageServerToQuery, LspStore,
LspStoreEvent, ProgressToken, SERVER_PROGRESS_THROTTLE_TIMEOUT,
LanguageServerPromptRequest, LanguageServerShowDocumentRequest, LanguageServerStatus,
LanguageServerToQuery, LspStore, LspStoreEvent, ProgressToken,
SERVER_PROGRESS_THROTTLE_TIMEOUT,
};
pub use toolchain_store::{ToolchainStore, Toolchains};
const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
@@ -363,6 +364,7 @@ pub enum Event {
notification_id: SharedString,
},
LanguageServerPrompt(LanguageServerPromptRequest),
LanguageServerShowDocument(LanguageServerShowDocumentRequest),
LanguageNotFound(Entity<Buffer>),
ActiveEntryChanged(Option<ProjectEntryId>),
ActivateProjectPanel,
@@ -1675,6 +1677,8 @@ impl Project {
remote_proto.add_entity_message_handler(Self::handle_toast);
remote_proto.add_entity_message_handler(Self::handle_telemetry_event);
remote_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
remote_proto
.add_entity_request_handler(Self::handle_language_server_show_document_request);
remote_proto.add_entity_message_handler(Self::handle_hide_toast);
remote_proto.add_entity_request_handler(Self::handle_update_buffer_from_remote_server);
remote_proto.add_entity_request_handler(Self::handle_trust_worktrees);
@@ -3756,6 +3760,9 @@ impl Project {
LspStoreEvent::LanguageServerPrompt(prompt) => {
cx.emit(Event::LanguageServerPrompt(prompt.clone()))
}
LspStoreEvent::LanguageServerShowDocument(request) => {
cx.emit(Event::LanguageServerShowDocument(request.clone()))
}
LspStoreEvent::DiskBasedDiagnosticsStarted { language_server_id } => {
cx.emit(Event::DiskBasedDiagnosticsStarted {
language_server_id: *language_server_id,
@@ -5614,6 +5621,49 @@ impl Project {
})
}
async fn handle_language_server_show_document_request(
project: Entity<Self>,
envelope: TypedEnvelope<proto::LanguageServerShowDocumentRequest>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let payload = envelope.payload;
let selection = payload.selection_start.zip(payload.selection_end).map(
|(selection_start, selection_end)| lsp::Range {
start: lsp::Position {
line: selection_start.row,
character: selection_start.column,
},
end: lsp::Position {
line: selection_end.row,
character: selection_end.column,
},
},
);
let uri = lsp::Uri::from_str(&payload.uri)
.with_context(|| format!("parsing show document uri {}", payload.uri))?;
let (tx, rx) = async_channel::bounded(1);
project.update(&mut cx, |_, cx| {
cx.emit(Event::LanguageServerShowDocument(
LanguageServerShowDocumentRequest {
uri,
external: payload.external,
take_focus: payload.take_focus,
selection,
response_channel: tx,
},
));
});
drop(project);
let success = rx.recv().await.unwrap_or(false);
anyhow::ensure!(
success,
"show document request for {} was not handled successfully",
payload.uri
);
Ok(proto::Ack {})
}
async fn handle_hide_toast(
this: Entity<Self>,
envelope: TypedEnvelope<proto::HideToast>,
+20
View File
@@ -960,6 +960,26 @@ message LanguageServerPromptResponse {
optional uint64 action_response = 1;
}
message LanguageServerShowDocumentRequest {
uint64 project_id = 1;
string uri = 2;
bool external = 3;
bool take_focus = 4;
optional PointUtf16 selection_start = 5;
optional PointUtf16 selection_end = 6;
}
message ExecuteLspCommand {
uint64 project_id = 1;
uint64 language_server_id = 2;
string command = 3;
repeated string arguments = 4;
}
message ExecuteLspCommandResponse {
optional string result = 1;
}
message GetDocumentColor {
uint64 project_id = 1;
uint64 buffer_id = 2;
+4 -1
View File
@@ -518,7 +518,10 @@ message Envelope {
GetOutgoingCalls get_outgoing_calls = 486;
GetOutgoingCallsResponse get_outgoing_calls_response = 487;
LspExtExpandAbbreviation lsp_ext_expand_abbreviation = 488;
LspExtExpandAbbreviationResponse lsp_ext_expand_abbreviation_response = 489; // current max
LspExtExpandAbbreviationResponse lsp_ext_expand_abbreviation_response = 489;
LanguageServerShowDocumentRequest language_server_show_document_request = 490;
ExecuteLspCommand execute_lsp_command = 491;
ExecuteLspCommandResponse execute_lsp_command_response = 492; // current max
}
reserved 87 to 88;
+7
View File
@@ -179,6 +179,7 @@ messages!(
(LanguageServerLog, Foreground),
(LanguageServerPromptRequest, Foreground),
(LanguageServerPromptResponse, Foreground),
(LanguageServerShowDocumentRequest, Background),
(LeaveChannelBuffer, Background),
(LeaveChannelChat, Foreground),
(LeaveProject, Foreground),
@@ -203,6 +204,8 @@ messages!(
(LspExtSwitchSourceHeaderResponse, Background),
(LspExtGoToParentModule, Background),
(LspExtGoToParentModuleResponse, Background),
(ExecuteLspCommand, Background),
(ExecuteLspCommandResponse, Background),
(LspExtCancelFlycheck, Background),
(LspExtRunFlycheck, Background),
(LspExtClearFlycheck, Background),
@@ -578,6 +581,7 @@ request_messages!(
(StopLanguageServers, Ack),
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
(LspExtGoToParentModule, LspExtGoToParentModuleResponse),
(ExecuteLspCommand, ExecuteLspCommandResponse),
(LspExtCancelFlycheck, Ack),
(LspExtRunFlycheck, Ack),
(LspExtClearFlycheck, Ack),
@@ -589,6 +593,7 @@ request_messages!(
(GetPermalinkToLine, GetPermalinkToLineResponse),
(FlushBufferedMessages, Ack),
(LanguageServerPromptRequest, LanguageServerPromptResponse),
(LanguageServerShowDocumentRequest, Ack),
(GitGetBranches, GitBranchesResponse),
(UpdateGitBranch, Ack),
(ListToolchains, ListToolchainsResponse),
@@ -809,6 +814,7 @@ entity_messages!(
LspExtRunnables,
LspExtSwitchSourceHeader,
LspExtGoToParentModule,
ExecuteLspCommand,
LspExtCancelFlycheck,
LspExtRunFlycheck,
LspExtClearFlycheck,
@@ -820,6 +826,7 @@ entity_messages!(
GetFilePermalink,
GetPermalinkToLine,
LanguageServerPromptRequest,
LanguageServerShowDocumentRequest,
GitGetBranches,
UpdateGitBranch,
ListToolchains,
@@ -479,6 +479,33 @@ impl HeadlessProject {
})
.log_err();
}
LspStoreEvent::LanguageServerShowDocument(show_document_request) => {
let request = self
.session
.request(proto::LanguageServerShowDocumentRequest {
project_id: REMOTE_SERVER_PROJECT_ID,
uri: show_document_request.uri.as_str().to_owned(),
external: show_document_request.external,
take_focus: show_document_request.take_focus,
selection_start: show_document_request.selection.map(|selection| {
proto::PointUtf16 {
row: selection.start.line,
column: selection.start.character,
}
}),
selection_end: show_document_request.selection.map(|selection| {
proto::PointUtf16 {
row: selection.end.line,
column: selection.end.character,
}
}),
});
let show_document_request = show_document_request.clone();
cx.background_spawn(async move {
show_document_request.respond(request.await.is_ok());
})
.detach();
}
LspStoreEvent::LanguageServerPrompt(prompt) => {
let request = self.session.request(proto::LanguageServerPromptRequest {
project_id: REMOTE_SERVER_PROJECT_ID,
@@ -4391,6 +4391,242 @@ async fn test_remote_apply_code_action_skips_unadvertised_command(
assert_eq!(transaction.0.len(), 0);
}
#[gpui::test]
async fn test_remote_lsp_show_document(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"src": {
"lib.rs": "fn one() -> usize { 1 }",
"other.rs": "fn two() -> usize { 2 }"
}
},
}),
)
.await;
let (project, headless) = init_test(&fs, cx, server_cx).await;
cx.update_entity(&project, |project, _| {
project.languages().register_test_language(LanguageConfig {
name: "Rust".into(),
matcher: (LanguageMatcher {
path_suffixes: vec!["rs".into()],
..LanguageMatcher::default()
})
.into(),
..LanguageConfig::default()
});
project.languages().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
..FakeLspAdapter::default()
},
)
});
let mut fake_lsp = server_cx.update(|cx| {
headless.read(cx).languages.register_fake_lsp_server(
LanguageServerName("rust-analyzer".into()),
lsp::ServerCapabilities::default(),
None,
)
});
cx.run_until_parked();
let worktree_id = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |worktree, _| worktree.id());
cx.run_until_parked();
let (_buffer, _handle) = project
.update(cx, |project, cx| {
project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
cx.run_until_parked();
let fake_lsp = fake_lsp.next().await.unwrap();
let shown_uri = lsp::Uri::from_file_path(path!("/code/project1/src/other.rs")).unwrap();
let shown_selection = lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6));
let handled_requests = Arc::new(AtomicUsize::new(0));
cx.update({
let project = project.clone();
let shown_uri = shown_uri.clone();
let handled_requests = handled_requests.clone();
move |cx| {
cx.subscribe(&project, move |_, event, _| {
if let project::Event::LanguageServerShowDocument(request) = event {
assert_eq!(request.uri, shown_uri);
assert_eq!(request.selection, Some(shown_selection));
assert_eq!((request.external, request.take_focus), (false, true));
handled_requests.fetch_add(1, Ordering::Release);
request.clone().respond(true);
}
})
.detach();
}
});
let response = fake_lsp
.request::<lsp::request::ShowDocument>(
lsp::ShowDocumentParams {
uri: shown_uri,
external: None,
take_focus: Some(true),
selection: Some(shown_selection),
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await
.into_response()
.expect("show document request should not error");
assert_eq!(response, lsp::ShowDocumentResult { success: true });
assert_eq!(handled_requests.load(Ordering::Acquire), 1);
}
#[gpui::test]
async fn test_remote_execute_lsp_command(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, headless) = init_test(&fs, cx, server_cx).await;
cx.update_entity(&project, |project, _| {
project.languages().register_test_language(LanguageConfig {
name: "Rust".into(),
matcher: (LanguageMatcher {
path_suffixes: vec!["rs".into()],
..LanguageMatcher::default()
})
.into(),
..LanguageConfig::default()
});
project.languages().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
..FakeLspAdapter::default()
},
)
});
let mut fake_lsp = server_cx.update(|cx| {
headless.read(cx).languages.register_fake_lsp_server(
LanguageServerName("rust-analyzer".into()),
lsp::ServerCapabilities {
execute_command_provider: Some(lsp::ExecuteCommandOptions {
commands: vec!["the-command".to_string()],
..lsp::ExecuteCommandOptions::default()
}),
..lsp::ServerCapabilities::default()
},
Some(Box::new(|fake| {
fake.set_request_handler::<lsp::request::ExecuteCommand, _, _>(
|params, _| async move {
assert_eq!(params.command, "the-command");
assert_eq!(params.arguments, vec![json!("foo"), json!(42)]);
Ok(Some(json!({"answer": 3})))
},
);
})),
)
});
cx.run_until_parked();
let worktree_id = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |worktree, _| worktree.id());
cx.run_until_parked();
let (_buffer, _handle) = project
.update(cx, |project, cx| {
project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
cx.run_until_parked();
let _fake_lsp = fake_lsp.next().await.unwrap();
let server_id = cx.read(|cx| {
project
.read(cx)
.language_server_statuses(cx)
.next()
.expect("a language server should be running")
.0
});
let result = project
.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.execute_lsp_command(
server_id,
"the-command".to_string(),
vec![json!("foo"), json!(42)],
cx,
)
})
})
.await
.expect("executing an advertised command should succeed");
assert_eq!(result, Some(json!({"answer": 3})));
let unadvertised = project
.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.execute_lsp_command(
server_id,
"unadvertised-command".to_string(),
Vec::new(),
cx,
)
})
})
.await;
assert_eq!(
unadvertised
.err()
.map(|error| format!("{error:#}").contains("not advertised")),
Some(true),
"unadvertised commands should be rejected"
);
}
#[gpui::test]
async fn test_remote_restore_unstaged_hunk_clears_diff(
cx: &mut TestAppContext,
+1
View File
@@ -161,6 +161,7 @@ language_selector.workspace = true
language_tools.workspace = true
languages = { workspace = true, features = ["load-grammars"] }
line_ending_selector.workspace = true
lsp_command_selector.workspace = true
log.workspace = true
lsp_locations.workspace = true
markdown.workspace = true
+1
View File
@@ -762,6 +762,7 @@ fn main() {
encoding_selector::init(cx);
language_selector::init(cx);
line_ending_selector::init(cx);
lsp_command_selector::init(cx);
toolchain_selector::init(cx);
theme_selector::init(cx);
settings_profile_selector::init(cx);
+1
View File
@@ -5935,6 +5935,7 @@ mod tests {
"language_selector",
"welcome",
"line_ending_selector",
"lsp_command_selector",
"lsp_tool",
"markdown",
"menu",