Skip to content

Add Call Hierarchy to the Outline panel - #64704

Open
swiftcoder wants to merge 4 commits into
zed-industries:mainfrom
swiftcoder:call-hierarchy/panel
Open

swiftcoder wants to merge 4 commits into
zed-industries:mainfrom
swiftcoder:call-hierarchy/panel

Conversation

@swiftcoder

@swiftcoder swiftcoder commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Objective

This revives the user-facing portion of @aohoyd's original #43750 PR. I'm very grateful the maintainers merged the underlying technology in #53239 - this PR aims to generate discussion about the panel UX itself.

If accepted, this change would close #14203.

Solution

Add the Call Hierarchy to the existing Outline panel.

Testing

I'm testing this actively locally on MacOS, but as I don't typically use outline views in my workflow, that is a bit of a learning experience.

Self-Review Checklist:

  • I've reviewed my own diff for quality, security, and reliability
  • Unsafe blocks (if any) have justifying comments
  • The content adheres to Zed's UI standards (UX/UI and icon guidelines)
  • Tests cover the new/changed behavior
  • Performance impact has been considered and is acceptable

Showcase

Screenshot 2026-09-25 at 14 45 48

Release Notes:

  • Added a call hierarchy view to the outline panel

@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Sep 24, 2026

@SomeoneToIgnore SomeoneToIgnore left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I have concerns about the icon design and the fact that it's yet another button in the status bar below — that was one of the reasons to exclude this, apart from the complexity.

I wonder if we could somehow repurpose the outline panel for that?

Intellij with their dropdown might serve as an example:

Image

@SomeoneToIgnore SomeoneToIgnore self-assigned this Sep 24, 2026
@SomeoneToIgnore

SomeoneToIgnore commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Also, I do not see any attribution to the original author, is that intended?

@swiftcoder

swiftcoder commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

I have concerns about the icon design

Yep, I would definitely like zed-industries/design view on that before any merge

the fact that it's yet another button in the status bar below... I wonder if we could somehow repurpose the outline panel for that?

I think that's a fair callout. My one concern with merging it into another panel is that folks may want both the call hierarchy and the outline visible at the same time.

The current layout is more-or-less inspired by VSCode's call hierarchy panel, but they have a more flexible panel system that allows arbitrarily stacking panels within each docking area.

Also, I do not see any attribution to the original author, is that intended?

Not intentional. Commit amended to add aohoyd as co-author.

@SomeoneToIgnore

Copy link
Copy Markdown
Contributor

may want both the call hierarchy and the outline visible at the same time

May be right as well, I have no good answer to that besides that you can call either outline or call hierarchy modals now.
But, I do not agree that we can ship this icon and another button too, so, unless we find a better solution, I'd stick to the combined panel — worst case, we can later work on duplicating the panel to the side, so we have 2 concurrent outline panels at cost of 0 design work involved.

like zed-industries/design view

I think we should not expect any here and move on.

@SomeoneToIgnore SomeoneToIgnore added the area:language server An umbrella label for all language servers label Sep 24, 2026
Adds a call hierarchy view mode to the outline panel instead of a
dedicated panel. Entered from the editor via "Show Call Hierarchy"
(context menu / app menu), seeded from the symbol under the cursor.

- New ItemsDisplayMode::CallHierarchy holding a lazily-fetched call tree
- New PanelEntry::CallHierarchy row type, rendered through the existing
  entry pipeline (icons, indentation, indent guides, selection, filter)
- Expand-on-demand via LSP incoming/outgoing calls; click navigates to
  the call site (opening other files as needed)
- In-panel header to toggle Incoming/Outgoing and return to the outline

Co-Authored-By: aohoyd <32036729+aohoyd@users.noreply.github.com>
@swiftcoder

Copy link
Copy Markdown
Contributor Author

Well, it's certainly a much slimmer change when reusing the outline panel's existing functionality. It is a slightly uncomfortable fusion, because outline is very focused on the active editor, and call hierarchy necessarily spans a bunch of editors (I've left some comments in the code where I'm worried the effects may be surprising).

I have tested this locally, and it works well, but I haven't stressed it heavily yet - let me know if this seems directionally sound to you, and I'll dig into the weeds if so.

@SomeoneToIgnore SomeoneToIgnore left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great that the diff smaller, but so far looks like it's a totally vibed code without any attempt to compare with the existing outline panel flow?

I would strongly suggest running the app and using the outline panel and comparing with the new mode, otherwise we'll risk to drown in review rounds and eventually get the PR closed, unmerged.

I think we would also benefit from another submodule with a set of tests for the new mode and some code moved there?

And, sure, we need screenshots and docs updated.

Some(pane),
buffer,
true,
true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pass prefer_focus_change through here rather than always focusing the editor?
select_next and select_previous call scroll_editor_to_entry with false, but the hierarchy branch ignores that preference and reaches this focus_item = true.
This appears to transfer focus to the editor after the first Down/Shift-Down, preventing subsequent presses from navigating the hierarchy.
The existing outline-navigation path keeps focus in the panel in this case.

state.root = None;
state.loading = true;
state.expanding.clear();
let origin = state.origin_buffer.clone().zip(state.origin_position);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we retain an anchor or reuse the root item instead of storing and reusing a raw PointUtf16?
For example, show the hierarchy for the first function, insert another function above it, then toggle direction.
The original function moves from (0, 3) to (1, 3), but this prepares the hierarchy again at (0, 3), now inside the inserted function.
The existing modal keeps its root item when switching direction (CallHierarchyView::set_mode).

.any(|((existing, _), incoming)| existing != incoming)
}
ItemsDisplayMode::Outline => true,
ItemsDisplayMode::Outline | ItemsDisplayMode::CallHierarchy(_) => true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep ordinary search refreshes from replacing an explicitly selected call hierarchy?
With Outline already visible and Find showing nonempty results, enter hierarchy mode and insert whitespace in the same editor.
The retained SearchEvent::MatchesInvalidated subscription schedules a contents refresh, which reaches this unconditional true and replaces CallHierarchy with Search below.
That discards the hierarchy and its expanded branches even when the search query and matches have not changed.

window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we resolve the selected excerpt instead of requiring a singleton?
This returns early for multibuffers, although Show Call Hierarchy is still offered in their editor context menu.
Invoking it from a project-search result or another excerpted editor therefore only focuses the panel without starting a hierarchy.
The existing modal already resolves the cursor to its underlying buffer and anchor with text_anchor_for_position in CallHierarchyDelegate::fetch_root.

) -> Task<(Vec<CachedEntry>, Option<usize>)> {
if matches!(self.mode, ItemsDisplayMode::CallHierarchy(_)) {
let entries = self.call_hierarchy_cached_entries(query.as_deref());
return Task::ready((entries, None));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we provide a width-measurement index for the hierarchy entries here?
UniformList::with_width_from_item(None) measures row zero, so the horizontal scroll extent is based only on the root rather than a wider child.
With a short root such as f and a caller whose name exceeds the panel width, the caller does not contribute to that extent, leaving its overflowing text unreachable by horizontal scrolling.
The normal entry-generation path tracks a width-measurement row, but this early return bypasses it.

if row.state == CallNodeState::Expanded {
self.toggle_call_node(row.id, window, cx);
}
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we preserve parent/child keyboard navigation when no expansion-state change is needed?
Left on a leaf returns here without selecting its parent, and the matching branch in expand_selected_entry returns on an expanded node without selecting its child.
Both bypass the existing navigation fallbacks below.
The new PanelEntry::CallHierarchy(_) => None arm in select_parent also falls back to the first row, so on a tree A -> B -> C, selecting C's parent selects A instead of B.

.upgrade()
.and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
{
self.replace_active_editor(active_item, active_editor, window, cx);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we preserve the explicit outline pin and avoid replacing an unchanged active editor here?
Pin file A, enter hierarchy mode, then choose Back to Outline without switching or closing A.
replace_active_editor calls clear_previous, which resets pinned to false, so merely entering and exiting hierarchy mode removes the user's pin.
The normal replacement path has same-item and pin checks in should_replace_active_item, but this call bypasses them.

- maintain state as an actual tree with parent references
- store anchors instead of raw positions
- ensure keyboard navigation is maintained throughout
- add some relevant ui tests
- draft documentation updates
@swiftcoder swiftcoder changed the title Add Call Hierarchy panel Add Call Hierarchy to the Outline panel Sep 25, 2026
@swiftcoder

Copy link
Copy Markdown
Contributor Author

Working through these gradually. In full transparency I don't typically to use the outline in my workflow, so there is a little bit of a learning curve figuring out how it is intended to be used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:language server An umbrella label for all language servers cla-signed The user has signed the Contributor License Agreement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add 'Show Call Hierarchy' panel

2 participants