Files
zed/crates/editor/src/rust_analyzer_ext.rs
T
Carl Jackson b84621eb77 Implement Vim's tag stack (#46002)
Happy New Years! This PR is a second take at
https://github.com/zed-industries/zed/pull/38127 (cc @ConradIrwin)

This PR is significantly less complicated than the last attempt: while
we still keep our data on the `NavigationHistory` object, we no longer
tightly integrate it with the existing back/forward "browser history."
Instead, we keep our own stack of `(origin, target)` pairs (in a struct
to make it easy to extend with e.g., tag names in the future).

The PR is split into two separable commits. Most of the implementation
is in the second commit, which:
- Defines the stack data structure
- Implements `pane::GoToOlderTag` and `pane::GoToNewerTag` in terms of
the stack
- Hooks into `navigate_to_hover_links` to push tag stack entries

This last bit is the most fiddly. The core challenge is that we need to
keep track of the `origin` location and calculate the `target` location
across three codepaths that might involve creating a new editor and/or
splitting the pane. One thing in particular I found difficult was that
an editor's `nav_history` (an `ItemNavHistory`) seems to be populated
asynchronously. Instead of relying on it, I decided in this code to make
my own `ItemNavHistory`. I briefly tried to refactor the code in
question, but it seemed like it would significantly increase the scope
of the change.

I prefer this all-in-one tracking centered around
`navigate_to_hover_links ` to the `start/finish` approach taken in
https://github.com/zed-industries/zed/commit/aebd394fb60641a2dd32b1f1b76a7c79ec4aa2ea
because I find it easier to convince myself that the right data is being
populated at the right times. Of course, let me know if you think
there's a better solution.

Closes #14206

Release Notes:
- ??? I don't know what to write here! Suggestions welcome
2026-01-15 17:48:15 +00:00

402 lines
13 KiB
Rust

use std::{fs, path::Path};
use anyhow::Context as _;
use gpui::{App, AppContext as _, Context, Entity, Window};
use language::{Capability, Language, proto::serialize_anchor};
use multi_buffer::MultiBuffer;
use project::{
ProjectItem,
lsp_command::location_link_from_proto,
lsp_store::{
lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
rust_analyzer_ext::{RUST_ANALYZER_NAME, cancel_flycheck, clear_flycheck, run_flycheck},
},
};
use rpc::proto;
use text::ToPointUtf16;
use crate::{
CancelFlycheck, ClearFlycheck, Editor, ExpandMacroRecursively, GoToParentModule,
GotoDefinitionKind, OpenDocs, RunFlycheck, element::register_action, hover_links::HoverLink,
lsp_ext::find_specific_language_server_in_selection,
};
fn is_rust_language(language: &Language) -> bool {
language.name() == "Rust".into()
}
pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
if editor.read(cx).project().is_some_and(|project| {
project
.read(cx)
.language_server_statuses(cx)
.any(|(_, status)| status.name == RUST_ANALYZER_NAME)
}) {
register_action(editor, window, cancel_flycheck_action);
register_action(editor, window, run_flycheck_action);
register_action(editor, window, clear_flycheck_action);
}
if editor
.read(cx)
.buffer()
.read(cx)
.all_buffers()
.into_iter()
.filter_map(|buffer| buffer.read(cx).language())
.any(|language| is_rust_language(language))
{
register_action(editor, window, go_to_parent_module);
register_action(editor, window, expand_macro_recursively);
register_action(editor, window, open_docs);
}
}
pub fn go_to_parent_module(
editor: &mut Editor,
_: &GoToParentModule,
window: &mut Window,
cx: &mut Context<Editor>,
) {
if editor.selections.count() == 0 {
return;
}
let Some(project) = &editor.project else {
return;
};
let Some((trigger_anchor, _, server_to_query, buffer)) =
find_specific_language_server_in_selection(
editor,
cx,
is_rust_language,
RUST_ANALYZER_NAME,
)
else {
return;
};
let nav_entry = editor.navigation_entry(editor.selections.newest_anchor().head(), cx);
let project = project.clone();
let lsp_store = project.read(cx).lsp_store();
let upstream_client = lsp_store.read(cx).upstream_client();
cx.spawn_in(window, async move |editor, cx| {
let location_links = if let Some((client, project_id)) = upstream_client {
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
let request = proto::LspExtGoToParentModule {
project_id,
buffer_id: buffer_id.to_proto(),
position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
};
let response = client
.request(request)
.await
.context("lsp ext go to parent module proto request")?;
futures::future::join_all(
response
.links
.into_iter()
.map(|link| location_link_from_proto(link, lsp_store.clone(), cx)),
)
.await
.into_iter()
.collect::<anyhow::Result<_>>()
.context("go to parent module via collab")?
} else {
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
project
.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
project::lsp_store::lsp_ext_command::GoToParentModule { position },
cx,
)
})
.await
.context("go to parent module")?
};
editor
.update_in(cx, |editor, window, cx| {
editor.navigate_to_hover_links(
Some(GotoDefinitionKind::Declaration),
location_links.into_iter().map(HoverLink::Text).collect(),
nav_entry,
false,
window,
cx,
)
})?
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
pub fn expand_macro_recursively(
editor: &mut Editor,
_: &ExpandMacroRecursively,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let Some(project) = &editor.project else {
return;
};
let Some(workspace) = editor.workspace() else {
return;
};
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;
};
let project = project.clone();
let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
cx.spawn_in(window, async move |_editor, cx| {
let macro_expansion = if let Some((client, project_id)) = upstream_client {
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id());
let request = proto::LspExtExpandMacro {
project_id,
buffer_id: buffer_id.to_proto(),
position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
};
let response = client
.request(request)
.await
.context("lsp ext expand macro proto request")?;
ExpandedMacro {
name: response.name,
expansion: response.expansion,
}
} else {
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
project
.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
ExpandMacro { position },
cx,
)
})
.await
.context("expand macro")?
};
if macro_expansion.is_empty() {
log::info!(
"Empty macro expansion for position {:?}",
trigger_anchor.text_anchor
);
return Ok(());
}
let buffer = project
.update(cx, |project, cx| {
project.create_buffer(Some(rust_language), false, cx)
})
.await?;
workspace.update_in(cx, |workspace, window, cx| {
buffer.update(cx, |buffer, cx| {
buffer.set_text(macro_expansion.expansion, cx);
buffer.set_capability(Capability::ReadOnly, cx);
});
let multibuffer =
cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name));
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
editor.set_read_only(true);
editor
})),
None,
true,
window,
cx,
);
})
})
.detach_and_log_err(cx);
}
pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mut Context<Editor>) {
if editor.selections.count() == 0 {
return;
}
let Some(project) = &editor.project else {
return;
};
let Some(workspace) = editor.workspace() else {
return;
};
let Some((trigger_anchor, _, server_to_query, buffer)) =
find_specific_language_server_in_selection(
editor,
cx,
is_rust_language,
RUST_ANALYZER_NAME,
)
else {
return;
};
let project = project.clone();
let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client();
cx.spawn_in(window, async move |_editor, cx| {
let docs_urls = if let Some((client, project_id)) = upstream_client {
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
let request = proto::LspExtOpenDocs {
project_id,
buffer_id: buffer_id.to_proto(),
position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
};
let response = client
.request(request)
.await
.context("lsp ext open docs proto request")?;
DocsUrls {
web: response.web,
local: response.local,
}
} else {
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
project
.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
project::lsp_store::lsp_ext_command::OpenDocs { position },
cx,
)
})
.await
.context("open docs")?
};
if docs_urls.is_empty() {
log::debug!(
"Empty docs urls for position {:?}",
trigger_anchor.text_anchor
);
return Ok(());
}
workspace.update(cx, |_workspace, cx| {
// Check if the local document exists, otherwise fallback to the online document.
// Open with the default browser.
if let Some(local_url) = docs_urls.local
&& fs::metadata(Path::new(&local_url[8..])).is_ok()
{
cx.open_url(&local_url);
return;
}
if let Some(web_url) = docs_urls.web {
cx.open_url(&web_url);
}
});
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn cancel_flycheck_action(
editor: &mut Editor,
_: &CancelFlycheck,
_: &mut Window,
cx: &mut Context<Editor>,
) {
let Some(project) = &editor.project else {
return;
};
let buffer_id = editor
.selections
.disjoint_anchors_arc()
.iter()
.find_map(|selection| {
let buffer_id = selection
.start
.text_anchor
.buffer_id
.or(selection.end.text_anchor.buffer_id)?;
let project = project.read(cx);
let entry_id = project
.buffer_for_id(buffer_id, cx)?
.read(cx)
.entry_id(cx)?;
project.path_for_entry(entry_id, cx)
});
cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
}
fn run_flycheck_action(
editor: &mut Editor,
_: &RunFlycheck,
_: &mut Window,
cx: &mut Context<Editor>,
) {
let Some(project) = &editor.project else {
return;
};
let buffer_id = editor
.selections
.disjoint_anchors_arc()
.iter()
.find_map(|selection| {
let buffer_id = selection
.start
.text_anchor
.buffer_id
.or(selection.end.text_anchor.buffer_id)?;
let project = project.read(cx);
let entry_id = project
.buffer_for_id(buffer_id, cx)?
.read(cx)
.entry_id(cx)?;
project.path_for_entry(entry_id, cx)
});
run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
}
fn clear_flycheck_action(
editor: &mut Editor,
_: &ClearFlycheck,
_: &mut Window,
cx: &mut Context<Editor>,
) {
let Some(project) = &editor.project else {
return;
};
let buffer_id = editor
.selections
.disjoint_anchors_arc()
.iter()
.find_map(|selection| {
let buffer_id = selection
.start
.text_anchor
.buffer_id
.or(selection.end.text_anchor.buffer_id)?;
let project = project.read(cx);
let entry_id = project
.buffer_for_id(buffer_id, cx)?
.read(cx)
.entry_id(cx)?;
project.path_for_entry(entry_id, cx)
});
clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
}