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:
1 parent
941bd6fa99
commit
c684a6a537
51 files changed
+4026
-82
No files matched your search
Generated
+27
-2
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -207,7 +207,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.log_err();
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
®.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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5840,6 +5840,7 @@ mod tests {
|
||||
"bedrock",
|
||||
"branches",
|
||||
"buffer_search",
|
||||
"call_hierarchy",
|
||||
"channel_modal",
|
||||
"cli",
|
||||
"client",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -816,6 +816,10 @@ actions!(
|
||||
OpenGitIntegrationOnboarding
|
||||
]
|
||||
);
|
||||
actions!(
|
||||
call_hierarchy,
|
||||
[ShowIncomingCalls, ShowOutgoingCalls, ToggleDirection]
|
||||
);
|
||||
|
||||
pub mod debug_panel {
|
||||
use gpui::actions;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user