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
Generated
+24
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -1222,6 +1222,12 @@
|
||||
// Editor bindings.
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "LspCommandSelector > Picker > Editor",
|
||||
"bindings": {
|
||||
"tab": "lsp_command_selector::ToggleArgumentsFocus",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal > Picker > Editor",
|
||||
"bindings": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1141,7 +1141,7 @@ impl LanguageServer {
|
||||
additional_properties_support: Some(true),
|
||||
}),
|
||||
}),
|
||||
..WindowClientCapabilities::default()
|
||||
show_document: Some(ShowDocumentClientCapabilities { support: true }),
|
||||
}),
|
||||
},
|
||||
trace: None,
|
||||
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
File diff suppressed because it is too large.
Load diff
+150
-20
@@ -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),
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5935,6 +5935,7 @@ mod tests {
|
||||
"language_selector",
|
||||
"welcome",
|
||||
"line_ending_selector",
|
||||
"lsp_command_selector",
|
||||
"lsp_tool",
|
||||
"markdown",
|
||||
"menu",
|
||||
|
||||
Reference in new issue
Block a user