clangd: Implement switch source/header extension (#14646)
Release Notes: - Added switch source/header action for clangd language server (fixes [#12801](https://github.com/zed-industries/zed/issues/12801)). Note: I'm new to both rust and this codebase. I started my implementation by copying how rust analyzer's "expand macro" LSP extension is implemented. I don't yet understand some of the code I copied (mostly the way to get the `server_to_query` in `clangd_ext.rs` and the whole proto implementation). --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
co-authored by
Kirill Bulatov
parent
96bcceed40
commit
f85ca387a7
@@ -306,6 +306,7 @@ gpui::actions!(
|
||||
SortLinesCaseInsensitive,
|
||||
SortLinesCaseSensitive,
|
||||
SplitSelectionIntoLines,
|
||||
SwitchSourceHeader,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleAutoSignatureHelp,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{View, ViewContext, WindowContext};
|
||||
use language::Language;
|
||||
use url::Url;
|
||||
|
||||
use crate::lsp_ext::find_specific_language_server_in_selection;
|
||||
|
||||
use crate::{element::register_action, Editor, SwitchSourceHeader};
|
||||
|
||||
static CLANGD_SERVER_NAME: &str = "clangd";
|
||||
|
||||
fn is_c_language(language: &Language) -> bool {
|
||||
return language.name().as_ref() == "C++" || language.name().as_ref() == "C";
|
||||
}
|
||||
|
||||
pub fn switch_source_header(
|
||||
editor: &mut Editor,
|
||||
_: &SwitchSourceHeader,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) {
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((_, _, server_to_query, buffer)) =
|
||||
find_specific_language_server_in_selection(&editor, cx, &is_c_language, CLANGD_SERVER_NAME)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = project.clone();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let source_file = buffer_snapshot
|
||||
.file()
|
||||
.unwrap()
|
||||
.file_name(cx)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
|
||||
let switch_source_header_task = project.update(cx, |project, cx| {
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_ext_command::SwitchSourceHeader,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.spawn(|_editor, mut cx| async move {
|
||||
let switch_source_header = switch_source_header_task
|
||||
.await
|
||||
.with_context(|| format!("Switch source/header LSP request for path \"{}\" failed", source_file))?;
|
||||
if switch_source_header.0.is_empty() {
|
||||
log::info!("Clangd returned an empty string when requesting to switch source/header from \"{}\"", source_file);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let goto = Url::parse(&switch_source_header.0).with_context(|| {
|
||||
format!(
|
||||
"Parsing URL \"{}\" returned from switch source/header failed",
|
||||
switch_source_header.0
|
||||
)
|
||||
})?;
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, view_cx| {
|
||||
workspace.open_abs_path(PathBuf::from(goto.path()), false, view_cx)
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Switch source/header could not open \"{}\" in workspace",
|
||||
goto.path()
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.map(|_| ())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
|
||||
if editor.update(cx, |e, cx| {
|
||||
find_specific_language_server_in_selection(e, cx, &is_c_language, CLANGD_SERVER_NAME)
|
||||
.is_some()
|
||||
}) {
|
||||
register_action(editor, cx, switch_source_header);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
pub mod actions;
|
||||
mod blame_entry_tooltip;
|
||||
mod blink_manager;
|
||||
mod clangd_ext;
|
||||
mod debounced_delay;
|
||||
pub mod display_map;
|
||||
mod editor_settings;
|
||||
@@ -30,6 +31,7 @@ mod inlay_hint_cache;
|
||||
mod inline_completion_provider;
|
||||
pub mod items;
|
||||
mod linked_editing_ranges;
|
||||
mod lsp_ext;
|
||||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod persistence;
|
||||
|
||||
@@ -165,6 +165,7 @@ impl EditorElement {
|
||||
});
|
||||
|
||||
crate::rust_analyzer_ext::apply_related_actions(view, cx);
|
||||
crate::clangd_ext::apply_related_actions(view, cx);
|
||||
register_action(view, cx, Editor::move_left);
|
||||
register_action(view, cx, Editor::move_right);
|
||||
register_action(view, cx, Editor::move_down);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Editor;
|
||||
use gpui::{Model, WindowContext};
|
||||
use language::Buffer;
|
||||
use language::Language;
|
||||
use lsp::LanguageServerId;
|
||||
use multi_buffer::Anchor;
|
||||
|
||||
pub(crate) fn find_specific_language_server_in_selection<F>(
|
||||
editor: &Editor,
|
||||
cx: &WindowContext,
|
||||
filter_language: F,
|
||||
language_server_name: &str,
|
||||
) -> Option<(Anchor, Arc<Language>, LanguageServerId, Model<Buffer>)>
|
||||
where
|
||||
F: Fn(&Language) -> bool,
|
||||
{
|
||||
let Some(project) = &editor.project else {
|
||||
return None;
|
||||
};
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.into_iter()
|
||||
.filter(|selection| selection.start == selection.end)
|
||||
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
|
||||
.filter_map(|(buffer_id, trigger_anchor)| {
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
|
||||
if !filter_language(&language) {
|
||||
return None;
|
||||
}
|
||||
Some((trigger_anchor, language, buffer))
|
||||
})
|
||||
.find_map(|(trigger_anchor, language, buffer)| {
|
||||
project
|
||||
.read(cx)
|
||||
.language_servers_for_buffer(buffer.read(cx), cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == language_server_name {
|
||||
Some((
|
||||
trigger_anchor,
|
||||
Arc::clone(&language),
|
||||
server.server_id(),
|
||||
buffer.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{Context, View, ViewContext, VisualContext, WindowContext};
|
||||
use language::Language;
|
||||
@@ -7,22 +5,24 @@ use multi_buffer::MultiBuffer;
|
||||
use project::lsp_ext_command::ExpandMacro;
|
||||
use text::ToPointUtf16;
|
||||
|
||||
use crate::{element::register_action, Editor, ExpandMacroRecursively};
|
||||
use crate::{
|
||||
element::register_action, lsp_ext::find_specific_language_server_in_selection, Editor,
|
||||
ExpandMacroRecursively,
|
||||
};
|
||||
|
||||
static RUST_ANALYZER_NAME: &str = "rust-analyzer";
|
||||
|
||||
fn is_rust_language(language: &Language) -> bool {
|
||||
language.name().as_ref() == "Rust"
|
||||
}
|
||||
|
||||
pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
|
||||
let is_rust_related = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.all_buffers()
|
||||
.iter()
|
||||
.any(|b| match b.read(cx).language() {
|
||||
Some(l) => is_rust_language(l),
|
||||
None => false,
|
||||
})
|
||||
});
|
||||
|
||||
if is_rust_related {
|
||||
if editor
|
||||
.update(cx, |e, cx| {
|
||||
find_specific_language_server_in_selection(e, cx, &is_rust_language, RUST_ANALYZER_NAME)
|
||||
})
|
||||
.is_some()
|
||||
{
|
||||
register_action(editor, cx, expand_macro_recursively);
|
||||
}
|
||||
}
|
||||
@@ -42,39 +42,13 @@ pub fn expand_macro_recursively(
|
||||
return;
|
||||
};
|
||||
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
|
||||
let Some((trigger_anchor, rust_language, server_to_query, buffer)) = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.into_iter()
|
||||
.filter(|selection| selection.start == selection.end)
|
||||
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
|
||||
.filter_map(|(buffer_id, trigger_anchor)| {
|
||||
let buffer = multibuffer.buffer(buffer_id)?;
|
||||
let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
|
||||
if !is_rust_language(&rust_language) {
|
||||
return None;
|
||||
}
|
||||
Some((trigger_anchor, rust_language, buffer))
|
||||
})
|
||||
.find_map(|(trigger_anchor, rust_language, buffer)| {
|
||||
project
|
||||
.read(cx)
|
||||
.language_servers_for_buffer(buffer.read(cx), cx)
|
||||
.find_map(|(adapter, server)| {
|
||||
if adapter.name.0.as_ref() == "rust-analyzer" {
|
||||
Some((
|
||||
trigger_anchor,
|
||||
Arc::clone(&rust_language),
|
||||
server.server_id(),
|
||||
buffer.clone(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
let Some((trigger_anchor, rust_language, server_to_query, buffer)) =
|
||||
find_specific_language_server_in_selection(
|
||||
&editor,
|
||||
cx,
|
||||
&is_rust_language,
|
||||
RUST_ANALYZER_NAME,
|
||||
)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -120,7 +94,3 @@ pub fn expand_macro_recursively(
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn is_rust_language(language: &Language) -> bool {
|
||||
language.name().as_ref() == "Rust"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user