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

+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;