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:
1 parent
5697636cd5
commit
66a1d8d7cb
23 files changed
+2019
-29
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user