Add LSP call hierarchy modal (#53239)

<img width="1128" height="953" alt="image"
src="https://github.com/user-attachments/assets/b2017c0a-e25a-49d7-9342-d074014d085c"
/>

Closes #14203

Release Notes:
- Initial Call Hierarchy support in Zed

---------

Co-authored-by: Alexey Olshanskiy <gh@aohoy.dev>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
authored and GitHub committed 2026-08-27 16:21:41 +00:00
1 parent 941bd6fa99
commit c684a6a537
51 files changed
+4026 -82

No files matched your search

Generated
+27 -2
View File
@@ -2668,6 +2668,32 @@ dependencies = [
"workspace",
]
[[package]]
name = "call_hierarchy"
version = "0.1.0"
dependencies = [
"editor",
"futures 0.3.32",
"fuzzy",
"gpui",
"language",
"log",
"lsp",
"menu",
"picker",
"project",
"proto",
"serde_json",
"settings",
"theme",
"theme_settings",
"tree-sitter-rust",
"ui",
"util",
"workspace",
"zed_actions",
]
[[package]]
name = "calloop"
version = "0.14.3"
@@ -9958,7 +9984,6 @@ dependencies = [
"picker",
"project",
"ui",
"util",
"workspace",
]
@@ -12169,7 +12194,6 @@ dependencies = [
"gpui",
"picker",
"project",
"schemars 1.0.4",
"serde",
"serde_json",
"settings",
@@ -22721,6 +22745,7 @@ dependencies = [
"auto_update_ui",
"breadcrumbs",
"call",
"call_hierarchy",
"channel",
"chrono",
"clap",
+2
View File
@@ -24,6 +24,7 @@ members = [
"crates/breadcrumbs",
"crates/buffer_diff",
"crates/call",
"crates/call_hierarchy",
"crates/channel",
"crates/cli",
"crates/client",
@@ -295,6 +296,7 @@ bedrock = { path = "crates/bedrock" }
breadcrumbs = { path = "crates/breadcrumbs" }
buffer_diff = { path = "crates/buffer_diff" }
call = { path = "crates/call" }
call_hierarchy = { path = "crates/call_hierarchy" }
channel = { path = "crates/channel" }
cli = { path = "crates/cli" }
client = { path = "crates/client" }
+9
View File
@@ -569,6 +569,7 @@
"shift-f12": "editor::GoToImplementation",
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-k ctrl-h": "call_hierarchy::ShowIncomingCalls",
"ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
"ctrl-|": "editor::MoveToEnclosingBracket",
"ctrl-{": "editor::Fold",
@@ -1184,6 +1185,14 @@
"tab": "channel_modal::ToggleMode",
},
},
{
"context": "CallHierarchyPicker > Picker > Editor",
"bindings": {
"ctrl-k ctrl-h": "call_hierarchy::ToggleDirection",
"ctrl-k right": "menu::SelectChild",
"ctrl-k left": "menu::SelectParent",
},
},
{
"context": "Picker > Editor",
"bindings": {
+10
View File
@@ -608,6 +608,7 @@
"shift-f12": "editor::GoToImplementation",
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"cmd-k cmd-h": "call_hierarchy::ShowIncomingCalls",
"cmd-|": "editor::MoveToEnclosingBracket",
"ctrl-m": "editor::MoveToEnclosingBracket", // From Jetbrains
"alt-cmd-[": "editor::Fold",
@@ -1231,6 +1232,15 @@
"tab": "channel_modal::ToggleMode",
},
},
{
"context": "CallHierarchyPicker > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-k cmd-h": "call_hierarchy::ToggleDirection",
"cmd-k right": "menu::SelectChild",
"cmd-k left": "menu::SelectParent",
},
},
{
"context": "Picker > Editor",
"use_key_equivalents": true,
+10
View File
@@ -568,6 +568,7 @@
"alt-f12": "editor::GoToDefinitionSplit",
"ctrl-f12": "editor::GoToImplementation",
"shift-alt-f12": "editor::FindAllReferences",
"ctrl-k ctrl-h": "call_hierarchy::ShowIncomingCalls",
"ctrl-shift-\\": "editor::MoveToEnclosingBracket",
"ctrl-shift-[": "editor::Fold",
"ctrl-shift-]": "editor::UnfoldLines",
@@ -1187,6 +1188,15 @@
"tab": "channel_modal::ToggleMode",
},
},
{
"context": "CallHierarchyPicker > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-k ctrl-h": "call_hierarchy::ToggleDirection",
"ctrl-k right": "menu::SelectChild",
"ctrl-k left": "menu::SelectParent",
},
},
{
"context": "Picker > Editor",
"use_key_equivalents": true,
+8
View File
@@ -20,6 +20,7 @@
"ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
"ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
"ctrl-k": "assistant::InlineAssist",
"shift-alt-h": "call_hierarchy::ShowIncomingCalls",
},
},
{
@@ -79,4 +80,11 @@
"ctrl-k": "assistant::InlineAssist",
},
},
{
"context": "CallHierarchyPicker > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"shift-alt-h": "call_hierarchy::ToggleDirection",
},
},
]
+8
View File
@@ -83,6 +83,7 @@
"ctrl-q": "editor::Hover",
"ctrl-p": "editor::ShowSignatureHelp",
"ctrl-\\": "assistant::InlineAssist",
"ctrl-alt-h": "call_hierarchy::ShowIncomingCalls",
},
},
{
@@ -218,4 +219,11 @@
"escape": "editor::Cancel",
},
},
{
"context": "CallHierarchyPicker > Picker > Editor",
"bindings": {
"ctrl-alt-h": "call_hierarchy::ToggleDirection",
"ctrl-k": null,
},
},
]
+8
View File
@@ -23,6 +23,7 @@
// In VS Code `ctrl-k z` is Zen Mode; the word wrap toggle moves to `alt-z`.
"ctrl-k z": "workspace::ToggleCenteredLayout",
"ctrl-i": "assistant::InlineAssist", // Inline Chat
"shift-alt-h": "call_hierarchy::ShowIncomingCalls",
},
},
{
@@ -69,4 +70,11 @@
"f11": "debugger::StepInto",
},
},
{
"context": "CallHierarchyPicker > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"shift-alt-h": "call_hierarchy::ToggleDirection",
},
},
]
+8
View File
@@ -20,6 +20,7 @@
"cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
"cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
"cmd-k": "assistant::InlineAssist",
"shift-alt-h": "call_hierarchy::ShowIncomingCalls",
},
},
{
@@ -80,4 +81,11 @@
"cmd-k": "assistant::InlineAssist",
},
},
{
"context": "CallHierarchyPicker > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"shift-alt-h": "call_hierarchy::ToggleDirection",
},
},
]
+8
View File
@@ -82,6 +82,7 @@
"cmd-j": "editor::Hover",
"cmd-p": "editor::ShowSignatureHelp",
"cmd-\\": "assistant::InlineAssist",
"ctrl-alt-h": "call_hierarchy::ShowIncomingCalls",
},
},
{
@@ -222,4 +223,11 @@
"escape": "editor::Cancel",
},
},
{
"context": "CallHierarchyPicker > Picker > Editor",
"bindings": {
"ctrl-alt-h": "call_hierarchy::ToggleDirection",
"cmd-k": null,
},
},
]
+8
View File
@@ -25,6 +25,7 @@
// In VS Code `cmd-k z` is Zen Mode; the word wrap toggle moves to `alt-z`.
"cmd-k z": "workspace::ToggleCenteredLayout",
"cmd-i": "assistant::InlineAssist", // Inline Chat
"shift-alt-h": "call_hierarchy::ShowIncomingCalls",
},
},
{
@@ -79,4 +80,11 @@
"f11": "debugger::StepInto",
},
},
{
"context": "CallHierarchyPicker > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"shift-alt-h": "call_hierarchy::ToggleDirection",
},
},
]
+7
View File
@@ -1429,6 +1429,13 @@
// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
"enable_keep_preview_on_code_navigation": false,
},
// Settings related to the call hierarchy picker.
"call_hierarchy": {
// Determines how much space the call hierarchy picker can take up in relation to the available window width.
//
// Default: medium
"modal_max_width": "medium",
},
// Settings related to the file finder.
"file_finder": {
// Whether to show file icons in the file finder.
@@ -327,7 +327,7 @@ impl PickerDelegate for ToolPickerDelegate {
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.tool_picker
.update(cx, |_this, cx| cx.emit(DismissEvent))
.log_err();
.ok();
}
fn render_match(
+42
View File
@@ -0,0 +1,42 @@
[package]
name = "call_hierarchy"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/call_hierarchy.rs"
doctest = false
[dependencies]
editor.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
picker.workspace = true
project.workspace = true
proto.workspace = true
settings.workspace = true
theme.workspace = true
theme_settings.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
futures.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"] }
workspace = { workspace = true, features = ["test-support"] }
serde_json.workspace = true
theme = { workspace = true, features = ["test-support"] }
tree-sitter-rust.workspace = true
+1
View File
@@ -0,0 +1 @@
../../LICENSE-GPL
File diff suppressed because it is too large. Load diff
@@ -6,7 +6,7 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{Avatar, ListItem, ListItemSpacing, prelude::*};
use util::{ResultExt as _, TryFutureExt};
use util::TryFutureExt;
use workspace::ModalView;
pub struct ContactFinder {
@@ -133,9 +133,7 @@ impl PickerDelegate for ContactFinderDelegate {
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.parent
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
self.parent.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
}
fn render_match(
+8
View File
@@ -270,6 +270,14 @@ pub fn deploy_context_menu(
"Find All References",
Box::new(FindAllReferences::default()),
)
.action(
"Show Incoming Calls",
Box::new(zed_actions::ShowIncomingCalls),
)
.action(
"Show Outgoing Calls",
Box::new(zed_actions::ShowOutgoingCalls),
)
.separator()
.action("Rename Symbol", Box::new(Rename))
.action("Format Buffer", Box::new(Format))
@@ -305,7 +305,7 @@ impl PickerDelegate for EncodingSelectorDelegate {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.encoding_selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
.ok();
}
fn render_match(
@@ -202,9 +202,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
self.selector.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
}
fn render_match(
+7 -18
View File
@@ -16,19 +16,16 @@ use fuzzy_nucleo::{PathMatch, PathMatchCandidate};
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, TaskExt,
WeakEntity, Window, actions, rems,
WeakEntity, Window, actions,
};
use language::{BufferSnapshot, Point};
use open_path_prompt::{
OpenPathPrompt,
file_finder_settings::{FileFinderSettings, FileFinderWidth},
};
use open_path_prompt::{OpenPathPrompt, file_finder_settings::FileFinderSettings};
use picker::{Picker, PickerDelegate};
use project::{
PathMatchCandidateSet, Project, ProjectPath, WorktreeId, worktree_store::WorktreeStore,
};
use settings::{Settings, SettingsStore};
use settings::{ModalWidthContent, Settings, SettingsStore};
use std::{
borrow::Cow,
cmp, mem,
@@ -325,17 +322,9 @@ impl FileFinder {
});
}
pub fn modal_max_width(width_setting: FileFinderWidth, window: &mut Window) -> Pixels {
let window_width = window.viewport_size().width;
let small_width = rems(34.).to_pixels(window.rem_size());
match width_setting {
FileFinderWidth::Small => small_width,
FileFinderWidth::Full => window_width,
FileFinderWidth::XLarge => (window_width - px(512.)).max(small_width),
FileFinderWidth::Large => (window_width - px(768.)).max(small_width),
FileFinderWidth::Medium => (window_width - px(1024.)).max(small_width),
}
pub fn modal_max_width(width_setting: ModalWidthContent, window: &mut Window) -> Pixels {
let small_width = picker::DEFAULT_MODAL_WIDTH.to_pixels(window.rem_size());
width_setting.to_pixels(small_width, window.viewport_size().width)
}
}
@@ -2059,7 +2048,7 @@ impl PickerDelegate for FileFinderDelegate {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<FileFinderDelegate>>) {
self.file_finder
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
.ok();
}
fn try_get_preview_data_for_match(&self, cx: &App) -> Option<picker::PreviewUpdate> {
@@ -232,7 +232,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.language_selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
.ok();
}
fn selected_index(&self) -> usize {
-1
View File
@@ -19,5 +19,4 @@ language.workspace = true
picker.workspace = true
project.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
@@ -8,7 +8,6 @@ use picker::{Picker, PickerDelegate};
use project::Project;
use std::sync::Arc;
use ui::{ListItem, ListItemSpacing, prelude::*};
use util::ResultExt;
use workspace::ModalView;
actions!(
@@ -146,7 +145,7 @@ impl PickerDelegate for LineEndingSelectorDelegate {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.line_ending_selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
.ok();
}
fn selected_index(&self) -> usize {
+3
View File
@@ -1012,6 +1012,9 @@ impl LanguageServer {
code_lens: Some(CodeLensClientCapabilities {
dynamic_registration: Some(true),
}),
call_hierarchy: Some(CallHierarchyClientCapabilities {
dynamic_registration: Some(true),
}),
document_symbol: Some(DocumentSymbolClientCapabilities {
hierarchical_document_symbol_support: Some(true),
dynamic_registration: Some(true),
+1 -1
View File
@@ -207,7 +207,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.log_err();
.ok();
}
fn render_match(
-1
View File
@@ -12,7 +12,6 @@ fuzzy.workspace = true
gpui.workspace = true
picker.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
ui.workspace = true
@@ -1,11 +1,10 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{RegisterSetting, Settings};
use serde::Deserialize;
use settings::{ModalWidthContent, RegisterSetting, Settings};
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, RegisterSetting)]
pub struct FileFinderSettings {
pub file_icons: bool,
pub modal_max_width: FileFinderWidth,
pub modal_max_width: ModalWidthContent,
pub skip_focus_for_active_in_search: bool,
pub include_ignored: Option<bool>,
pub include_channels: bool,
@@ -17,7 +16,7 @@ impl Settings for FileFinderSettings {
Self {
file_icons: file_finder.file_icons.unwrap(),
modal_max_width: file_finder.modal_max_width.unwrap().into(),
modal_max_width: file_finder.modal_max_width.unwrap(),
skip_focus_for_active_in_search: file_finder.skip_focus_for_active_in_search.unwrap(),
include_ignored: match file_finder.include_ignored.unwrap() {
settings::IncludeIgnoredContent::All => Some(true),
@@ -28,26 +27,3 @@ impl Settings for FileFinderSettings {
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum FileFinderWidth {
#[default]
Small,
Medium,
Large,
XLarge,
Full,
}
impl From<settings::FileFinderWidthContent> for FileFinderWidth {
fn from(content: settings::FileFinderWidthContent) -> Self {
match content {
settings::FileFinderWidthContent::Small => FileFinderWidth::Small,
settings::FileFinderWidthContent::Medium => FileFinderWidth::Medium,
settings::FileFinderWidthContent::Large => FileFinderWidth::Large,
settings::FileFinderWidthContent::XLarge => FileFinderWidth::XLarge,
settings::FileFinderWidthContent::Full => FileFinderWidth::Full,
}
}
}
+1 -2
View File
@@ -16,7 +16,6 @@ use settings::Settings;
use theme::ActiveTheme;
use theme_settings::ThemeSettings;
use ui::{ListItem, ListItemSpacing, prelude::*};
use util::ResultExt;
use workspace::{DismissDecision, ModalView};
pub fn init(cx: &mut App) {
@@ -434,7 +433,7 @@ impl PickerDelegate for OutlineViewDelegate {
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<OutlineViewDelegate>>) {
self.outline_view
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
.ok();
self.restore_active_editor(window, cx);
}
+620 -2
View File
@@ -17,14 +17,14 @@ use futures::future;
use gpui::{App, AsyncApp, Entity, SharedString, Task, TaskExt, prelude::FluentBuilder};
use language::{
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CharScopeContext,
OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
OffsetRangeExt, PointUtf16, SymbolKind, ToOffset, ToPointUtf16, Transaction, Unclipped,
language_settings::{InlayHintKind, LanguageSettings},
lsp_to_symbol_kind, point_from_lsp, point_to_lsp,
proto::{
deserialize_anchor, deserialize_anchor_range, deserialize_markup_kind, deserialize_version,
serialize_anchor, serialize_anchor_range, serialize_markup_kind, serialize_version,
},
range_from_lsp, range_to_lsp,
range_from_lsp, range_to_lsp, symbol_kind_to_lsp,
};
use lsp::{
AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription,
@@ -344,6 +344,624 @@ pub struct GetDocumentDiagnostics {
pub previous_result_id: Option<SharedString>,
}
#[derive(Debug, Clone)]
pub struct CallHierarchyItem {
pub buffer: Entity<Buffer>,
pub server_id: LanguageServerId,
pub name: String,
pub kind: lsp::SymbolKind,
pub detail: Option<String>,
pub range: Range<Anchor>,
pub selection_range: Range<Anchor>,
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct PrepareCallHierarchy {
pub position: PointUtf16,
}
async fn call_hierarchy_item_from_lsp(
item: lsp::CallHierarchyItem,
server_id: LanguageServerId,
lsp_store: &Entity<LspStore>,
cx: &mut AsyncApp,
) -> Result<CallHierarchyItem> {
let buffer = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.open_local_buffer_via_lsp(item.uri, server_id, cx)
})
.await?;
let (range, selection_range) = buffer.read_with(cx, |buffer, _| {
(
anchor_range_from_lsp(item.range, buffer),
anchor_range_from_lsp(item.selection_range, buffer),
)
});
Ok(CallHierarchyItem {
buffer,
server_id,
name: item.name,
kind: item.kind,
detail: item.detail,
range,
selection_range,
data: item.data,
})
}
fn anchor_range_from_lsp(range: lsp::Range, buffer: &Buffer) -> Range<Anchor> {
let start = buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
let end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
buffer.anchor_after(start)..buffer.anchor_before(end)
}
fn call_hierarchy_item_to_lsp(
item: &CallHierarchyItem,
path: &Path,
buffer: &Buffer,
) -> Result<lsp::CallHierarchyItem> {
Ok(lsp::CallHierarchyItem {
name: item.name.clone(),
kind: item.kind,
tags: None,
detail: item.detail.clone(),
uri: file_path_to_lsp_url(path)?,
range: range_to_lsp(item.range.to_point_utf16(buffer))?,
selection_range: range_to_lsp(item.selection_range.to_point_utf16(buffer))?,
data: item.data.clone(),
})
}
fn call_hierarchy_item_to_proto(
item: &CallHierarchyItem,
buffer_id: BufferId,
) -> proto::CallHierarchyItem {
proto::CallHierarchyItem {
name: item.name.clone(),
kind: lsp_to_symbol_kind(item.kind).to_proto(),
detail: item.detail.clone(),
buffer_id: buffer_id.into(),
server_id: item.server_id.to_proto(),
range_start: Some(serialize_anchor(&item.range.start)),
range_end: Some(serialize_anchor(&item.range.end)),
selection_range_start: Some(serialize_anchor(&item.selection_range.start)),
selection_range_end: Some(serialize_anchor(&item.selection_range.end)),
data: item.data.as_ref().map(|data| data.to_string().into_bytes()),
}
}
fn call_hierarchy_item_response_to_proto(
item: &CallHierarchyItem,
lsp_store: &mut LspStore,
peer_id: PeerId,
cx: &mut App,
) -> proto::CallHierarchyItem {
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&item.buffer, peer_id, cx)
})
.detach_and_log_err(cx);
let buffer_id = item.buffer.read(cx).remote_id();
call_hierarchy_item_to_proto(item, buffer_id)
}
fn call_hierarchy_item_fields_from_proto(
item: proto::CallHierarchyItem,
buffer: Entity<Buffer>,
) -> Result<CallHierarchyItem> {
let range = item
.range_start
.and_then(deserialize_anchor)
.zip(item.range_end.and_then(deserialize_anchor))
.context("invalid item range")?;
let selection_range = item
.selection_range_start
.and_then(deserialize_anchor)
.zip(item.selection_range_end.and_then(deserialize_anchor))
.context("invalid item selection range")?;
Ok(CallHierarchyItem {
buffer,
server_id: LanguageServerId::from_proto(item.server_id),
name: item.name,
kind: symbol_kind_to_lsp(SymbolKind::from_proto(item.kind)),
detail: item.detail,
range: range.0..range.1,
selection_range: selection_range.0..selection_range.1,
data: item
.data
.map(|data| serde_json::from_slice(&data))
.transpose()?,
})
}
async fn call_hierarchy_item_from_proto(
item: proto::CallHierarchyItem,
lsp_store: &Entity<LspStore>,
cx: &mut AsyncApp,
) -> Result<CallHierarchyItem> {
let buffer_id = BufferId::new(item.buffer_id)?;
let buffer = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})
.await?;
let item = call_hierarchy_item_fields_from_proto(item, buffer)?;
item.buffer
.update(cx, |buffer, _| {
buffer.wait_for_anchors([
item.range.start,
item.range.end,
item.selection_range.start,
item.selection_range.end,
])
})
.await?;
Ok(item)
}
#[async_trait(?Send)]
impl LspCommand for PrepareCallHierarchy {
type Response = Vec<CallHierarchyItem>;
type LspRequest = lsp::request::CallHierarchyPrepare;
type ProtoRequest = proto::PrepareCallHierarchy;
fn display_name(&self) -> &str {
"Prepare call hierarchy"
}
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
capabilities
.server_capabilities
.call_hierarchy_provider
.as_ref()
.is_some_and(|capability| match capability {
lsp::CallHierarchyServerCapability::Simple(supported) => *supported,
lsp::CallHierarchyServerCapability::Options(_) => true,
})
}
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &App,
) -> Result<lsp::CallHierarchyPrepareParams> {
Ok(lsp::CallHierarchyPrepareParams {
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
})
}
async fn response_from_lsp(
self,
message: Option<Vec<lsp::CallHierarchyItem>>,
lsp_store: Entity<LspStore>,
_buffer: Entity<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncApp,
) -> Result<Vec<CallHierarchyItem>> {
let mut items = Vec::new();
for item in message.unwrap_or_default() {
items.push(call_hierarchy_item_from_lsp(item, server_id, &lsp_store, &mut cx).await?);
}
Ok(items)
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PrepareCallHierarchy {
proto::PrepareCallHierarchy {
project_id,
buffer_id: buffer.remote_id().into(),
position: Some(serialize_anchor(&buffer.anchor_before(self.position))),
version: serialize_version(&buffer.version()),
}
}
async fn from_proto(
message: proto::PrepareCallHierarchy,
_: Entity<LspStore>,
buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Self> {
let position = message
.position
.and_then(deserialize_anchor)
.context("invalid position")?;
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&message.version))
})
.await?;
Ok(Self {
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
})
}
fn response_to_proto(
response: Vec<CallHierarchyItem>,
lsp_store: &mut LspStore,
peer_id: PeerId,
_: &clock::Global,
cx: &mut App,
) -> proto::PrepareCallHierarchyResponse {
proto::PrepareCallHierarchyResponse {
items: response
.iter()
.map(|item| call_hierarchy_item_response_to_proto(item, lsp_store, peer_id, cx))
.collect(),
}
}
async fn response_from_proto(
self,
message: proto::PrepareCallHierarchyResponse,
lsp_store: Entity<LspStore>,
_: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Vec<CallHierarchyItem>> {
let mut items = Vec::new();
for item in message.items {
items.push(call_hierarchy_item_from_proto(item, &lsp_store, &mut cx).await?);
}
Ok(items)
}
fn buffer_id_from_proto(message: &proto::PrepareCallHierarchy) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
#[derive(Debug, Clone)]
pub struct IncomingCall {
pub from: CallHierarchyItem,
pub from_ranges: Vec<Location>,
}
#[derive(Debug, Clone)]
pub struct OutgoingCall {
pub to: CallHierarchyItem,
pub from_ranges: Vec<Location>,
}
#[derive(Debug, Clone)]
pub struct GetIncomingCalls {
pub item: CallHierarchyItem,
}
fn location_to_proto(
location: &Location,
lsp_store: &mut LspStore,
peer_id: PeerId,
cx: &mut App,
) -> proto::Location {
lsp_store
.buffer_store()
.update(cx, |buffer_store, cx| {
buffer_store.create_buffer_for_peer(&location.buffer, peer_id, cx)
})
.detach_and_log_err(cx);
proto::Location {
buffer_id: location.buffer.read(cx).remote_id().into(),
start: Some(serialize_anchor(&location.range.start)),
end: Some(serialize_anchor(&location.range.end)),
}
}
async fn location_from_proto(
location: proto::Location,
lsp_store: &Entity<LspStore>,
cx: &mut AsyncApp,
) -> Result<Location> {
let buffer_id = BufferId::new(location.buffer_id)?;
let buffer = lsp_store
.update(cx, |lsp_store, cx| {
lsp_store.wait_for_remote_buffer(buffer_id, cx)
})
.await?;
let start = location
.start
.and_then(deserialize_anchor)
.context("missing location start")?;
let end = location
.end
.and_then(deserialize_anchor)
.context("missing location end")?;
buffer
.update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?;
Ok(Location {
buffer,
range: start..end,
})
}
#[async_trait(?Send)]
impl LspCommand for GetIncomingCalls {
type Response = Vec<IncomingCall>;
type LspRequest = lsp::request::CallHierarchyIncomingCalls;
type ProtoRequest = proto::GetIncomingCalls;
fn display_name(&self) -> &str {
"Get incoming calls"
}
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
capabilities
.server_capabilities
.call_hierarchy_provider
.as_ref()
.is_some_and(|capability| match capability {
lsp::CallHierarchyServerCapability::Simple(supported) => *supported,
lsp::CallHierarchyServerCapability::Options(_) => true,
})
}
fn to_lsp(
&self,
path: &Path,
buffer: &Buffer,
_language_server: &Arc<LanguageServer>,
_cx: &App,
) -> Result<lsp::CallHierarchyIncomingCallsParams> {
Ok(lsp::CallHierarchyIncomingCallsParams {
item: call_hierarchy_item_to_lsp(&self.item, path, buffer)?,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
})
}
async fn response_from_lsp(
self,
message: Option<Vec<lsp::CallHierarchyIncomingCall>>,
lsp_store: Entity<LspStore>,
_buffer: Entity<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncApp,
) -> Result<Vec<IncomingCall>> {
let mut calls = Vec::new();
for call in message.unwrap_or_default() {
let from =
call_hierarchy_item_from_lsp(call.from, server_id, &lsp_store, &mut cx).await?;
let from_ranges = from.buffer.read_with(&cx, |buffer, _| {
call.from_ranges
.into_iter()
.map(|range| Location {
buffer: from.buffer.clone(),
range: anchor_range_from_lsp(range, buffer),
})
.collect()
});
calls.push(IncomingCall { from, from_ranges });
}
Ok(calls)
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetIncomingCalls {
proto::GetIncomingCalls {
project_id,
buffer_id: buffer.remote_id().into(),
item: Some(call_hierarchy_item_to_proto(&self.item, buffer.remote_id())),
version: serialize_version(&buffer.version()),
}
}
async fn from_proto(
message: proto::GetIncomingCalls,
_lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Self> {
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&message.version))
})
.await?;
Ok(Self {
item: call_hierarchy_item_fields_from_proto(
message.item.context("missing item")?,
buffer,
)?,
})
}
fn response_to_proto(
response: Vec<IncomingCall>,
lsp_store: &mut LspStore,
peer_id: PeerId,
_buffer_version: &clock::Global,
cx: &mut App,
) -> proto::GetIncomingCallsResponse {
proto::GetIncomingCallsResponse {
calls: response
.into_iter()
.map(|call| proto::CallHierarchyIncomingCall {
from: Some(call_hierarchy_item_response_to_proto(
&call.from, lsp_store, peer_id, cx,
)),
from_ranges: call
.from_ranges
.iter()
.map(|location| location_to_proto(location, lsp_store, peer_id, cx))
.collect(),
})
.collect(),
}
}
async fn response_from_proto(
self,
message: proto::GetIncomingCallsResponse,
lsp_store: Entity<LspStore>,
_buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Vec<IncomingCall>> {
let mut calls = Vec::new();
for call in message.calls {
let from = call_hierarchy_item_from_proto(
call.from.context("missing from")?,
&lsp_store,
&mut cx,
)
.await?;
let mut from_ranges = Vec::new();
for location in call.from_ranges {
from_ranges.push(location_from_proto(location, &lsp_store, &mut cx).await?);
}
calls.push(IncomingCall { from, from_ranges });
}
Ok(calls)
}
fn buffer_id_from_proto(message: &proto::GetIncomingCalls) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
#[derive(Debug, Clone)]
pub struct GetOutgoingCalls {
pub item: CallHierarchyItem,
}
#[async_trait(?Send)]
impl LspCommand for GetOutgoingCalls {
type Response = Vec<OutgoingCall>;
type LspRequest = lsp::request::CallHierarchyOutgoingCalls;
type ProtoRequest = proto::GetOutgoingCalls;
fn display_name(&self) -> &str {
"Get outgoing calls"
}
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
capabilities
.server_capabilities
.call_hierarchy_provider
.as_ref()
.is_some_and(|capability| match capability {
lsp::CallHierarchyServerCapability::Simple(supported) => *supported,
lsp::CallHierarchyServerCapability::Options(_) => true,
})
}
fn to_lsp(
&self,
path: &Path,
buffer: &Buffer,
_language_server: &Arc<LanguageServer>,
_cx: &App,
) -> Result<lsp::CallHierarchyOutgoingCallsParams> {
Ok(lsp::CallHierarchyOutgoingCallsParams {
item: call_hierarchy_item_to_lsp(&self.item, path, buffer)?,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
})
}
async fn response_from_lsp(
self,
message: Option<Vec<lsp::CallHierarchyOutgoingCall>>,
lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncApp,
) -> Result<Vec<OutgoingCall>> {
let mut calls = Vec::new();
for call in message.unwrap_or_default() {
let to = call_hierarchy_item_from_lsp(call.to, server_id, &lsp_store, &mut cx).await?;
let from_ranges = buffer.read_with(&cx, |queried_buffer, _| {
call.from_ranges
.into_iter()
.map(|range| Location {
buffer: buffer.clone(),
range: anchor_range_from_lsp(range, queried_buffer),
})
.collect()
});
calls.push(OutgoingCall { to, from_ranges });
}
Ok(calls)
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetOutgoingCalls {
proto::GetOutgoingCalls {
project_id,
buffer_id: buffer.remote_id().into(),
item: Some(call_hierarchy_item_to_proto(&self.item, buffer.remote_id())),
version: serialize_version(&buffer.version()),
}
}
async fn from_proto(
message: proto::GetOutgoingCalls,
_lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Self> {
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&message.version))
})
.await?;
Ok(Self {
item: call_hierarchy_item_fields_from_proto(
message.item.context("missing item")?,
buffer,
)?,
})
}
fn response_to_proto(
response: Vec<OutgoingCall>,
lsp_store: &mut LspStore,
peer_id: PeerId,
_buffer_version: &clock::Global,
cx: &mut App,
) -> proto::GetOutgoingCallsResponse {
proto::GetOutgoingCallsResponse {
calls: response
.into_iter()
.map(|call| proto::CallHierarchyOutgoingCall {
to: Some(call_hierarchy_item_response_to_proto(
&call.to, lsp_store, peer_id, cx,
)),
from_ranges: call
.from_ranges
.iter()
.map(|location| location_to_proto(location, lsp_store, peer_id, cx))
.collect(),
})
.collect(),
}
}
async fn response_from_proto(
self,
message: proto::GetOutgoingCallsResponse,
lsp_store: Entity<LspStore>,
_buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Vec<OutgoingCall>> {
let mut calls = Vec::new();
for call in message.calls {
let to =
call_hierarchy_item_from_proto(call.to.context("missing to")?, &lsp_store, &mut cx)
.await?;
let mut from_ranges = Vec::new();
for location in call.from_ranges {
from_ranges.push(location_from_proto(location, &lsp_store, &mut cx).await?);
}
calls.push(OutgoingCall { to, from_ranges });
}
Ok(calls)
}
fn buffer_id_from_proto(message: &proto::GetOutgoingCalls) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
#[async_trait(?Send)]
impl LspCommand for PrepareRename {
type Response = PrepareRenameResponse;
+236
View File
@@ -6885,6 +6885,189 @@ impl LspStore {
}
}
pub fn prepare_call_hierarchy(
&mut self,
buffer: &Entity<Buffer>,
position: PointUtf16,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<CallHierarchyItem>>>> {
if let Some((upstream_client, project_id)) = self.upstream_client() {
let request = PrepareCallHierarchy { position };
if !self.is_capable_for_proto_request(buffer, &request, cx) {
return Task::ready(Ok(None));
}
let request_timeout = ProjectSettings::get_global(cx)
.global_lsp_settings
.get_request_timeout();
let request_task = upstream_client.request_lsp(
project_id,
None,
request_timeout,
cx.background_executor().clone(),
request.to_proto(project_id, buffer.read(cx)),
);
let buffer = buffer.clone();
cx.spawn(async move |weak_lsp_store, cx| {
let Some(lsp_store) = weak_lsp_store.upgrade() else {
return Ok(None);
};
let Some(responses) = request_task.await? else {
return Ok(None);
};
let items = join_all(responses.payload.into_iter().map(|response| {
PrepareCallHierarchy { position }.response_from_proto(
response.response,
lsp_store.clone(),
buffer.clone(),
cx.clone(),
)
}))
.await;
Ok(Some(dedup_call_hierarchy_items(
items
.into_iter()
.collect::<Result<Vec<Vec<_>>>>()?
.into_iter()
.flatten(),
)))
})
} else {
let task = self.request_multiple_lsp_locally(
buffer,
Some(position),
PrepareCallHierarchy { position },
cx,
);
cx.background_spawn(async move {
Ok(Some(dedup_call_hierarchy_items(
task.await.into_iter().flat_map(|(_, items)| items),
)))
})
}
}
pub fn incoming_calls(
&mut self,
item: CallHierarchyItem,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<IncomingCall>>>> {
let buffer = item.buffer.clone();
let server_id = item.server_id;
if let Some((upstream_client, project_id)) = self.upstream_client() {
let request = GetIncomingCalls { item };
if !self.is_capable_for_proto_request(&buffer, &request, cx) {
return Task::ready(Ok(None));
}
let request_timeout = ProjectSettings::get_global(cx)
.global_lsp_settings
.get_request_timeout();
let request_task = upstream_client.request_lsp(
project_id,
Some(server_id.to_proto()),
request_timeout,
cx.background_executor().clone(),
request.to_proto(project_id, buffer.read(cx)),
);
cx.spawn(async move |weak_lsp_store, cx| {
let Some(lsp_store) = weak_lsp_store.upgrade() else {
return Ok(None);
};
let Some(responses) = request_task.await? else {
return Ok(None);
};
let calls = join_all(responses.payload.into_iter().map(|response| {
request.clone().response_from_proto(
response.response,
lsp_store.clone(),
buffer.clone(),
cx.clone(),
)
}))
.await;
Ok(Some(
calls
.into_iter()
.collect::<Result<Vec<Vec<_>>>>()?
.into_iter()
.flatten()
.collect(),
))
})
} else {
let task = self.request_lsp(
buffer,
LanguageServerToQuery::Other(server_id),
GetIncomingCalls { item },
cx,
);
cx.background_spawn(async move { Ok(Some(task.await?)) })
}
}
pub fn outgoing_calls(
&mut self,
item: CallHierarchyItem,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<OutgoingCall>>>> {
let buffer = item.buffer.clone();
let server_id = item.server_id;
if let Some((upstream_client, project_id)) = self.upstream_client() {
let request = GetOutgoingCalls { item };
if !self.is_capable_for_proto_request(&buffer, &request, cx) {
return Task::ready(Ok(None));
}
let request_timeout = ProjectSettings::get_global(cx)
.global_lsp_settings
.get_request_timeout();
let request_task = upstream_client.request_lsp(
project_id,
Some(server_id.to_proto()),
request_timeout,
cx.background_executor().clone(),
request.to_proto(project_id, buffer.read(cx)),
);
cx.spawn(async move |weak_lsp_store, cx| {
let Some(lsp_store) = weak_lsp_store.upgrade() else {
return Ok(None);
};
let Some(responses) = request_task.await? else {
return Ok(None);
};
let calls = join_all(responses.payload.into_iter().map(|response| {
request.clone().response_from_proto(
response.response,
lsp_store.clone(),
buffer.clone(),
cx.clone(),
)
}))
.await;
Ok(Some(
calls
.into_iter()
.collect::<Result<Vec<Vec<_>>>>()?
.into_iter()
.flatten()
.collect(),
))
})
} else {
let task = self.request_lsp(
buffer,
LanguageServerToQuery::Other(server_id),
GetOutgoingCalls { item },
cx,
);
cx.background_spawn(async move { Ok(Some(task.await?)) })
}
}
pub fn code_actions(
&mut self,
buffer: &Entity<Buffer>,
@@ -10023,6 +10206,43 @@ impl LspStore {
.await
.context("querying for inlay hints")?
}
Request::PrepareCallHierarchy(prepare_call_hierarchy) => {
let position = prepare_call_hierarchy.position.and_then(deserialize_anchor);
Self::query_lsp_locally::<PrepareCallHierarchy>(
lsp_store,
server_id,
sender_id,
lsp_request_id,
prepare_call_hierarchy,
position,
&mut cx,
)
.await?;
}
Request::GetIncomingCalls(get_incoming_calls) => {
Self::query_lsp_locally::<GetIncomingCalls>(
lsp_store,
server_id,
sender_id,
lsp_request_id,
get_incoming_calls,
None,
&mut cx,
)
.await?;
}
Request::GetOutgoingCalls(get_outgoing_calls) => {
Self::query_lsp_locally::<GetOutgoingCalls>(
lsp_store,
server_id,
sender_id,
lsp_request_id,
get_outgoing_calls,
None,
&mut cx,
)
.await?;
}
//////////////////////////////
// Below are LSP queries that need to fetch more data,
// hence cannot just proxy the request to language server with `query_lsp_locally`.
@@ -14055,6 +14275,22 @@ fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
}
}
fn dedup_call_hierarchy_items(
items: impl IntoIterator<Item = CallHierarchyItem>,
) -> Vec<CallHierarchyItem> {
let mut seen = HashSet::default();
items
.into_iter()
.filter(|item| {
seen.insert((
item.buffer.entity_id(),
item.selection_range.clone(),
item.name.clone(),
))
})
.collect()
}
fn deduplicate_hovers(hovers: Vec<Hover>) -> Vec<Hover> {
let mut unique_hovers = Vec::with_capacity(hovers.len());
for hover in hovers {
@@ -78,6 +78,7 @@ pub(super) struct DynamicRegistrations {
inlay_hint: CapabilityRegistrations<OneOf<bool, lsp::InlayHintServerCapabilities>>,
code_lens: CapabilityRegistrations<lsp::CodeLensOptions>,
document_symbol: CapabilityRegistrations<OneOf<bool, lsp::DocumentSymbolOptions>>,
call_hierarchy: CapabilityRegistrations<lsp::CallHierarchyServerCapability>,
}
impl LspStore {
@@ -435,6 +436,24 @@ impl LspStore {
|capabilities| &mut capabilities.code_action_provider,
)?;
}
"textDocument/prepareCallHierarchy" => {
let options = parse_register_capabilities(reg.register_options)?;
let provider = match options {
OneOf::Left(value) => lsp::CallHierarchyServerCapability::Simple(value),
OneOf::Right(options) => {
lsp::CallHierarchyServerCapability::Options(options)
}
};
self.register_dynamic_capability(
&server,
&reg.method,
reg.id,
provider,
cx,
|registrations| &mut registrations.call_hierarchy,
|capabilities| &mut capabilities.call_hierarchy_provider,
)?;
}
"textDocument/definition" => {
let options = parse_register_capabilities(reg.register_options)?;
self.register_dynamic_capability(
@@ -829,6 +848,15 @@ impl LspStore {
|capabilities| &mut capabilities.code_action_provider,
)?;
}
"textDocument/prepareCallHierarchy" => {
self.unregister_dynamic_capability(
&server,
unreg,
cx,
|registrations| &mut registrations.call_hierarchy,
|capabilities| &mut capabilities.call_hierarchy_provider,
)?;
}
"textDocument/definition" => {
self.unregister_dynamic_capability(
&server,
+51
View File
@@ -164,6 +164,7 @@ pub use task_inventory::{
};
pub use buffer_store::ProjectTransaction;
pub use lsp_command::{CallHierarchyItem, IncomingCall, OutgoingCall};
pub use lsp_store::{
DiagnosticSummary, InvalidationStrategy, LanguageServerLogType, LanguageServerProgress,
LanguageServerPromptRequest, LanguageServerStatus, LanguageServerToQuery, LspStore,
@@ -4451,6 +4452,56 @@ impl Project {
})
}
pub fn prepare_call_hierarchy<T: ToPointUtf16>(
&mut self,
buffer: &Entity<Buffer>,
position: T,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<CallHierarchyItem>>>> {
let position = position.to_point_utf16(buffer.read(cx));
let guard = self.retain_remotely_created_models(cx);
let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.prepare_call_hierarchy(buffer, position, cx)
});
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
})
}
pub fn incoming_calls(
&mut self,
item: CallHierarchyItem,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<IncomingCall>>>> {
let guard = self.retain_remotely_created_models(cx);
let task = self
.lsp_store
.update(cx, |lsp_store, cx| lsp_store.incoming_calls(item, cx));
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
})
}
pub fn outgoing_calls(
&mut self,
item: CallHierarchyItem,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<OutgoingCall>>>> {
let guard = self.retain_remotely_created_models(cx);
let task = self
.lsp_store
.update(cx, |lsp_store, cx| lsp_store.outgoing_calls(item, cx));
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
})
}
pub fn document_highlights<T: ToPointUtf16>(
&mut self,
buffer: &Entity<Buffer>,
+62
View File
@@ -117,6 +117,62 @@ message DocumentHighlight {
}
}
message CallHierarchyItem {
string name = 1;
int32 kind = 2;
optional string detail = 3;
uint64 buffer_id = 4;
uint64 server_id = 5;
Anchor range_start = 6;
Anchor range_end = 7;
Anchor selection_range_start = 8;
Anchor selection_range_end = 9;
optional bytes data = 10;
}
message CallHierarchyIncomingCall {
CallHierarchyItem from = 1;
repeated Location from_ranges = 2;
}
message CallHierarchyOutgoingCall {
CallHierarchyItem to = 1;
repeated Location from_ranges = 2;
}
message PrepareCallHierarchy {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
message PrepareCallHierarchyResponse {
repeated CallHierarchyItem items = 1;
}
message GetIncomingCalls {
uint64 project_id = 1;
uint64 buffer_id = 2;
CallHierarchyItem item = 3;
repeated VectorClockEntry version = 4;
}
message GetIncomingCallsResponse {
repeated CallHierarchyIncomingCall calls = 1;
}
message GetOutgoingCalls {
uint64 project_id = 1;
uint64 buffer_id = 2;
CallHierarchyItem item = 3;
repeated VectorClockEntry version = 4;
}
message GetOutgoingCallsResponse {
repeated CallHierarchyOutgoingCall calls = 1;
}
message GetProjectSymbols {
uint64 project_id = 1;
string query = 2;
@@ -946,6 +1002,9 @@ message LspQuery {
GetDocumentLinks get_document_links = 19;
GetEditPredictionDefinition get_edit_prediction_definition = 20;
GetEditPredictionTypeDefinition get_edit_prediction_type_definition = 21;
PrepareCallHierarchy prepare_call_hierarchy = 22;
GetIncomingCalls get_incoming_calls = 23;
GetOutgoingCalls get_outgoing_calls = 24;
}
}
@@ -975,6 +1034,9 @@ message LspResponse {
GetDocumentLinksResponse get_document_links_response = 17;
GetEditPredictionDefinitionResponse get_edit_prediction_definition_response = 18;
GetEditPredictionTypeDefinitionResponse get_edit_prediction_type_definition_response = 19;
PrepareCallHierarchyResponse prepare_call_hierarchy_response = 20;
GetIncomingCallsResponse get_incoming_calls_response = 21;
GetOutgoingCallsResponse get_outgoing_calls_response = 22;
}
uint64 server_id = 7;
}
+8 -1
View File
@@ -509,7 +509,14 @@ message Envelope {
BlameBufferAtRevision blame_buffer_at_revision = 478;
BlameBufferAtRevisionResponse blame_buffer_at_revision_response = 479;
GetFilePermalink get_file_permalink = 480;
GetFilePermalinkResponse get_file_permalink_response = 481; // current max
GetFilePermalinkResponse get_file_permalink_response = 481;
PrepareCallHierarchy prepare_call_hierarchy = 482;
PrepareCallHierarchyResponse prepare_call_hierarchy_response = 483;
GetIncomingCalls get_incoming_calls = 484;
GetIncomingCallsResponse get_incoming_calls_response = 485;
GetOutgoingCalls get_outgoing_calls = 486;
GetOutgoingCallsResponse get_outgoing_calls_response = 487; // current max
}
reserved 87 to 88;
+16 -1
View File
@@ -130,6 +130,10 @@ messages!(
(GetTypeDefinitionResponse, Background),
(GetImplementation, Background),
(GetImplementationResponse, Background),
(GetIncomingCalls, Background),
(GetIncomingCallsResponse, Background),
(GetOutgoingCalls, Background),
(GetOutgoingCallsResponse, Background),
(OpenUnstagedDiff, Foreground),
(OpenUnstagedDiffResponse, Foreground),
(OpenUncommittedDiff, Foreground),
@@ -203,6 +207,8 @@ messages!(
(PerformRename, Background),
(PerformRenameResponse, Background),
(Ping, Foreground),
(PrepareCallHierarchy, Background),
(PrepareCallHierarchyResponse, Background),
(PrepareRename, Background),
(PrepareRenameResponse, Background),
(ProjectEntryResponse, Foreground),
@@ -484,6 +490,9 @@ request_messages!(
(OpenNewBuffer, OpenBufferResponse),
(PerformRename, PerformRenameResponse),
(Ping, Ack),
(PrepareCallHierarchy, PrepareCallHierarchyResponse),
(GetIncomingCalls, GetIncomingCallsResponse),
(GetOutgoingCalls, GetOutgoingCallsResponse),
(PrepareRename, PrepareRenameResponse),
(RefreshInlayHints, Ack),
(RefreshSemanticTokens, Ack),
@@ -663,7 +672,10 @@ lsp_messages!(
(GetTypeDefinition, GetTypeDefinitionResponse, true),
(GetImplementation, GetImplementationResponse, true),
(InlayHints, InlayHintsResponse, false),
(SemanticTokens, SemanticTokensResponse, true)
(SemanticTokens, SemanticTokensResponse, true),
(PrepareCallHierarchy, PrepareCallHierarchyResponse, true),
(GetIncomingCalls, GetIncomingCallsResponse, true),
(GetOutgoingCalls, GetOutgoingCallsResponse, true),
);
entity_messages!(
@@ -1065,6 +1077,9 @@ impl LspQuery {
Some(lsp_query::Request::GetDocumentLinks(_)) => ("GetDocumentLinks", false),
Some(lsp_query::Request::InlayHints(_)) => ("InlayHints", false),
Some(lsp_query::Request::SemanticTokens(_)) => ("SemanticTokens", false),
Some(lsp_query::Request::PrepareCallHierarchy(_)) => ("PrepareCallHierarchy", false),
Some(lsp_query::Request::GetIncomingCalls(_)) => ("GetIncomingCalls", false),
Some(lsp_query::Request::GetOutgoingCalls(_)) => ("GetOutgoingCalls", false),
None => ("<unknown>", true),
}
}
@@ -30,7 +30,8 @@ use gpui::{
};
use http_client::{BlockedHttpClient, FakeHttpClient};
use language::{
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Point,
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
OffsetRangeExt, Point, PointUtf16,
language_settings::{AllLanguageSettings, ConfiguredLanguageServer, LanguageSettings},
};
use lsp::{
@@ -1120,6 +1121,182 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
})
}
#[gpui::test]
async fn test_remote_call_hierarchy(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 main() { helper(); }\nfn helper() {}\n"
}
},
}),
)
.await;
let (project, headless) = init_test(&fs, cx, server_cx).await;
fs.insert_tree(
path!("/code/project1/.zed"),
json!({
"settings.json": r#"
{
"languages": {"Rust":{"language_servers":["rust-analyzer"]}},
"lsp": {
"rust-analyzer": {
"binary": {
"path": "~/.cargo/bin/rust-analyzer"
}
}
}
}"#
}),
)
.await;
cx.update_entity(&project, |project, _| {
project.languages().register_test_language(LanguageConfig {
name: "Rust".into(),
matcher: Arc::new(LanguageMatcher {
path_suffixes: vec!["rs".into()],
..LanguageMatcher::default()
}),
..LanguageConfig::default()
});
project.languages().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
capabilities: lsp::ServerCapabilities {
call_hierarchy_provider: Some(lsp::CallHierarchyServerCapability::Simple(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
)
});
let mut fake_lsp = server_cx.update(|cx| {
headless.read(cx).languages.register_fake_lsp_server(
LanguageServerName("rust-analyzer".into()),
lsp::ServerCapabilities {
call_hierarchy_provider: Some(lsp::CallHierarchyServerCapability::Simple(true)),
..lsp::ServerCapabilities::default()
},
None,
)
});
cx.run_until_parked();
let worktree_id = project
.update(cx, |project, cx| {
project.languages().add(rust_lang());
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 test_uri = lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap();
fake_lsp.set_request_handler::<lsp::request::CallHierarchyPrepare, _, _>({
let uri = test_uri.clone();
move |_, _| {
let uri = uri.clone();
async move {
Ok(Some(vec![lsp::CallHierarchyItem {
name: "main".into(),
kind: lsp::SymbolKind::FUNCTION,
tags: None,
detail: Some("fn main()".into()),
uri,
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 21)),
selection_range: lsp::Range::new(
lsp::Position::new(0, 3),
lsp::Position::new(0, 7),
),
data: None,
}]))
}
}
});
fake_lsp.set_request_handler::<lsp::request::CallHierarchyOutgoingCalls, _, _>({
move |_, _| {
let uri = test_uri.clone();
async move {
Ok(Some(vec![lsp::CallHierarchyOutgoingCall {
to: lsp::CallHierarchyItem {
name: "helper".into(),
kind: lsp::SymbolKind::FUNCTION,
tags: None,
detail: Some("fn helper()".into()),
uri,
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 13)),
selection_range: lsp::Range::new(
lsp::Position::new(1, 3),
lsp::Position::new(1, 9),
),
data: None,
},
from_ranges: Vec::new(),
}]))
}
}
});
let items = project
.update(cx, |project, cx| {
project.prepare_call_hierarchy(&buffer, PointUtf16::new(0, 3), cx)
})
.await
.unwrap()
.unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].name, "main");
assert_eq!(items[0].buffer, buffer);
items[0].buffer.read_with(cx, |item_buffer, _| {
assert_eq!(
items[0].selection_range.to_point(item_buffer),
Point::new(0, 3)..Point::new(0, 7)
);
});
let outgoing_calls = project
.update(cx, |project, cx| {
project.outgoing_calls(items[0].clone(), cx)
})
.await
.unwrap()
.unwrap();
assert_eq!(outgoing_calls.len(), 1);
assert_eq!(outgoing_calls[0].to.name, "helper");
assert_eq!(outgoing_calls[0].to.buffer, buffer);
outgoing_calls[0].to.buffer.read_with(cx, |item_buffer, _| {
assert_eq!(
outgoing_calls[0].to.selection_range.to_point(item_buffer),
Point::new(1, 3)..Point::new(1, 9)
);
});
}
#[gpui::test]
async fn test_remote_code_action_resolve(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
cx.update(|cx| {
+9
View File
@@ -426,6 +426,15 @@ impl AnyProtoClient {
Response::GetDocumentLinksResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::PrepareCallHierarchyResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetIncomingCallsResponse(response) => {
to_any_envelope(&envelope, response)
}
Response::GetOutgoingCallsResponse(response) => {
to_any_envelope(&envelope, response)
}
};
Some(proto::ProtoLspResponse {
server_id,
+1
View File
@@ -184,6 +184,7 @@ impl VsCodeSettings {
diagnostics: None,
editor: self.editor_settings_content(),
extension: ExtensionSettingsContent::default(),
call_hierarchy: None,
file_finder: None,
git: self.git_settings_content(),
git_panel: self.git_panel_settings_content(),
@@ -193,6 +193,8 @@ pub struct SettingsContent {
/// Settings related to the file finder.
pub file_finder: Option<FileFinderSettingsContent>,
pub call_hierarchy: Option<CallHierarchySettingsContent>,
pub git_panel: Option<GitPanelSettingsContent>,
pub tabs: Option<ItemSettingsContent>,
@@ -397,8 +399,9 @@ impl SettingsContent {
fallible_options::flattened_deserialize!(SettingsContent {
sections: { project, theme, extension, workspace, editor, remote },
options: {
file_finder, git_panel, tabs, tab_bar, status_bar, preview_tabs, agent, agent_servers,
audio, auto_update, base_keymap, collaboration_panel, debugger, diagnostics, git,
call_hierarchy, file_finder, git_panel, tabs, tab_bar, status_bar, preview_tabs, agent,
agent_servers, audio, auto_update, base_keymap, collaboration_panel, debugger, diagnostics,
git,
global_lsp_settings, image_viewer, markdown_preview, repl, helix_mode, hide_mouse,
journal, log, line_indicator_format, language_models, outline_panel, project_panel,
node, proxy, reduce_motion, server_url, credentials_url, session, telemetry, terminal,
@@ -931,7 +934,7 @@ pub struct FileFinderSettingsContent {
/// Determines how much space the file finder can take up in relation to the available window width.
///
/// Default: small
pub modal_max_width: Option<FileFinderWidthContent>,
pub modal_max_width: Option<ModalWidthContent>,
/// Determines whether the file finder should skip focus for the active file in search results.
///
/// Default: true
@@ -987,7 +990,7 @@ pub enum IncludeIgnoredContent {
strum::VariantNames,
)]
#[serde(rename_all = "lowercase")]
pub enum FileFinderWidthContent {
pub enum ModalWidthContent {
#[default]
Small,
Medium,
@@ -996,6 +999,27 @@ pub enum FileFinderWidthContent {
Full,
}
impl ModalWidthContent {
pub fn to_pixels(self, base_width: gpui::Pixels, window_width: gpui::Pixels) -> gpui::Pixels {
match self {
ModalWidthContent::Small => base_width,
ModalWidthContent::Full => window_width,
ModalWidthContent::XLarge => (window_width - gpui::px(512.)).max(base_width),
ModalWidthContent::Large => (window_width - gpui::px(768.)).max(base_width),
ModalWidthContent::Medium => (window_width - gpui::px(1024.)).max(base_width),
}
}
}
#[with_fallible_options]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
pub struct CallHierarchySettingsContent {
/// Determines how much space the call hierarchy picker can take up in relation to the available window width.
///
/// Default: medium
pub modal_max_width: Option<ModalWidthContent>,
}
#[with_fallible_options]
#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema, MergeFrom)]
pub struct VimSettingsContent {
+1 -1
View File
@@ -250,7 +250,7 @@ impl PickerDelegate for ScopeSelectorDelegate {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.scope_selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
.ok();
}
fn selected_index(&self) -> usize {
@@ -209,9 +209,7 @@ impl PickerDelegate for IconThemeSelectorDelegate {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<IconThemeSelectorDelegate>>) {
self.revert_theme(cx);
self.selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
self.selector.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
}
fn selected_index(&self) -> usize {
+1 -3
View File
@@ -418,9 +418,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
self.revert_theme(cx);
self.selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
self.selector.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
}
fn selected_index(&self) -> usize {
@@ -957,7 +957,7 @@ impl PickerDelegate for ToolchainSelectorDelegate {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.toolchain_selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
.ok();
}
fn selected_index(&self) -> usize {
+1
View File
@@ -86,6 +86,7 @@ auto_update.workspace = true
auto_update_ui.workspace = true
breadcrumbs.workspace = true
call.workspace = true
call_hierarchy.workspace = true
channel.workspace = true
chrono.workspace = true
clap.workspace = true
+1
View File
@@ -748,6 +748,7 @@ fn main() {
file_finder::init(cx);
tab_switcher::init(cx);
outline::init(cx);
call_hierarchy::init(cx);
project_symbols::init(cx);
project_panel::init(cx);
outline_panel::init(cx);
+1
View File
@@ -5840,6 +5840,7 @@ mod tests {
"bedrock",
"branches",
"buffer_search",
"call_hierarchy",
"channel_modal",
"cli",
"client",
+2
View File
@@ -248,6 +248,8 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
"Find All References",
editor::actions::FindAllReferences::default(),
),
MenuItem::action("Show Incoming Calls", call_hierarchy::ShowIncomingCalls),
MenuItem::action("Show Outgoing Calls", call_hierarchy::ShowOutgoingCalls),
MenuItem::separator(),
MenuItem::action("Next Problem", editor::actions::GoToDiagnostic::default()),
MenuItem::action(
+4
View File
@@ -816,6 +816,10 @@ actions!(
OpenGitIntegrationOnboarding
]
);
actions!(
call_hierarchy,
[ShowIncomingCalls, ShowOutgoingCalls, ToggleDirection]
);
pub mod debug_panel {
use gpui::actions;
+9
View File
@@ -42,6 +42,15 @@ Jump to where a symbol is defined with {#kb editor::GoToDefinition} (or `Cmd+Cli
- **Current file:** {#kb outline::Toggle} opens an outline of symbols in the active file
- **Entire project:** {#kb project_symbols::Toggle} searches symbols across all files
## Call Hierarchy
With the cursor on a function, {#action call_hierarchy::ShowIncomingCalls} ({#kb call_hierarchy::ShowIncomingCalls}) lists the functions that call it, and {#action call_hierarchy::ShowOutgoingCalls} lists the functions it calls.
Both open a picker; selecting an entry jumps to the call site, and the secondary confirm opens it in an adjacent pane.
While the picker is open, {#action call_hierarchy::ToggleDirection} switches between callers and callees, {#action menu::SelectChild} re-roots the hierarchy at the selected function to explore one level deeper, and {#action menu::SelectParent} steps back up; the picker's footer shows the corresponding key bindings.
Both actions are also available in the editor's right-click menu.
The modal's width can be changed via the `call_hierarchy.modal_max_width` setting.
This requires the language server to support call hierarchy.
## Outline Panel
The Outline Panel ({#kb outline_panel::ToggleFocus}) shows a persistent tree view of symbols in the current file. It's especially useful with [multibuffers](./multibuffers.md) for navigating search results or diagnostics.
+8
View File
@@ -3570,6 +3570,14 @@ Examples:
`boolean` values
## Call Hierarchy
### Modal Max Width
- Description: Max-width of the call hierarchy modal. It can take one of these values: `small`, `medium`, `large`, `xlarge`, and `full`.
- Setting: `modal_max_width`
- Default: `medium`
## File Finder
### File Icons