Cargo.lock 🔗
@@ -9247,6 +9247,7 @@ dependencies = [
"anyhow",
"client",
"collections",
+ "command_palette_hooks",
"copilot",
"editor",
"futures 0.3.31",
Finn Evers created
In an effort to improve the experience while developing extensions and
improving themes, this PR updates the syntax tree views behavior
slightly.
Before, the view would always update to the current active editor whilst
being used. This was quite painful for improving extension scheme files,
as you would always have to change back and forth between editors to
have a view at the relevant syntax tree.
With this PR, the syntax tree view will now stay attached to the editor
it was opened in, similar to preview views. Once the view is shown, the
`UseActiveEditor` will become available in the command palette and
enable the user to update the view to the last focused editor. On file
close, the view will also be updated accordingly.
https://github.com/user-attachments/assets/922075e5-9da0-4c1d-9e1a-51e024bf41ea
A button is also shown whenever switching is possible.
Futhermore, improved the empty state of the view.
Lastly, a drive-by cleanup of the `show_action_types` method so there is
no need to call `iter()` when calling the method.
Release Notes:
- The syntax tree view will now stay attached to the buffer it was
opened in, similar to the Markdown preview. Use the `UseActiveEditor`
action when the view is shown to change it to the last focused editor.
Cargo.lock | 1
crates/agent_ui/src/agent_ui.rs | 3
crates/command_palette_hooks/src/command_palette_hooks.rs | 4
crates/copilot/src/copilot.rs | 2
crates/language_tools/Cargo.toml | 1
crates/language_tools/src/syntax_tree_view.rs | 395 ++++++--
crates/settings_ui/src/settings_ui.rs | 2
crates/workspace/src/workspace.rs | 6
crates/zed/src/zed.rs | 3
crates/zeta/src/init.rs | 4
10 files changed, 302 insertions(+), 119 deletions(-)
@@ -9247,6 +9247,7 @@ dependencies = [
"anyhow",
"client",
"collections",
+ "command_palette_hooks",
"copilot",
"editor",
"futures 0.3.31",
@@ -337,8 +337,7 @@ fn update_command_palette_filter(cx: &mut App) {
];
filter.show_action_types(edit_prediction_actions.iter());
- filter
- .show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
+ filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
}
});
}
@@ -76,7 +76,7 @@ impl CommandPaletteFilter {
}
/// Hides all actions with the given types.
- pub fn hide_action_types(&mut self, action_types: &[TypeId]) {
+ pub fn hide_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
for action_type in action_types {
self.hidden_action_types.insert(*action_type);
self.shown_action_types.remove(action_type);
@@ -84,7 +84,7 @@ impl CommandPaletteFilter {
}
/// Shows all actions with the given types.
- pub fn show_action_types<'a>(&mut self, action_types: impl Iterator<Item = &'a TypeId>) {
+ pub fn show_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
for action_type in action_types {
self.shown_action_types.insert(*action_type);
self.hidden_action_types.remove(action_type);
@@ -1095,7 +1095,7 @@ impl Copilot {
_ => {
filter.hide_action_types(&signed_in_actions);
filter.hide_action_types(&auth_actions);
- filter.show_action_types(no_auth_actions.iter());
+ filter.show_action_types(&no_auth_actions);
}
}
}
@@ -16,6 +16,7 @@ doctest = false
anyhow.workspace = true
client.workspace = true
collections.workspace = true
+command_palette_hooks.workspace = true
copilot.workspace = true
editor.workspace = true
futures.workspace = true
@@ -1,17 +1,22 @@
+use command_palette_hooks::CommandPaletteFilter;
use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
use gpui::{
- App, AppContext as _, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, Hsla,
- InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement,
- Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle, WeakEntity, Window,
- actions, div, rems, uniform_list,
+ App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
+ Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
+ ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle,
+ WeakEntity, Window, actions, div, rems, uniform_list,
};
use language::{Buffer, OwnedSyntaxLayer};
-use std::{mem, ops::Range};
+use std::{any::TypeId, mem, ops::Range};
use theme::ActiveTheme;
use tree_sitter::{Node, TreeCursor};
-use ui::{ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex};
+use ui::{
+ ButtonCommon, ButtonLike, Clickable, Color, ContextMenu, FluentBuilder as _, IconButton,
+ IconName, Label, LabelCommon, LabelSize, PopoverMenu, StyledExt, Tooltip, h_flex, v_flex,
+};
use workspace::{
- SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+ Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation,
+ ToolbarItemView, Workspace,
item::{Item, ItemHandle},
};
@@ -19,17 +24,51 @@ actions!(
dev,
[
/// Opens the syntax tree view for the current file.
- OpenSyntaxTreeView
+ OpenSyntaxTreeView,
+ ]
+);
+
+actions!(
+ syntax_tree_view,
+ [
+ /// Update the syntax tree view to show the last focused file.
+ UseActiveEditor
]
);
pub fn init(cx: &mut App) {
- cx.observe_new(|workspace: &mut Workspace, _, _| {
- workspace.register_action(|workspace, _: &OpenSyntaxTreeView, window, cx| {
+ let syntax_tree_actions = [TypeId::of::<UseActiveEditor>()];
+
+ CommandPaletteFilter::update_global(cx, |this, _| {
+ this.hide_action_types(&syntax_tree_actions);
+ });
+
+ cx.observe_new(move |workspace: &mut Workspace, _, _| {
+ workspace.register_action(move |workspace, _: &OpenSyntaxTreeView, window, cx| {
+ CommandPaletteFilter::update_global(cx, |this, _| {
+ this.show_action_types(&syntax_tree_actions);
+ });
+
let active_item = workspace.active_item(cx);
let workspace_handle = workspace.weak_handle();
- let syntax_tree_view =
- cx.new(|cx| SyntaxTreeView::new(workspace_handle, active_item, window, cx));
+ let syntax_tree_view = cx.new(|cx| {
+ cx.on_release(move |view: &mut SyntaxTreeView, cx| {
+ if view
+ .workspace_handle
+ .read_with(cx, |workspace, cx| {
+ workspace.item_of_type::<SyntaxTreeView>(cx).is_none()
+ })
+ .unwrap_or_default()
+ {
+ CommandPaletteFilter::update_global(cx, |this, _| {
+ this.hide_action_types(&syntax_tree_actions);
+ });
+ }
+ })
+ .detach();
+
+ SyntaxTreeView::new(workspace_handle, active_item, window, cx)
+ });
workspace.split_item(
SplitDirection::Right,
Box::new(syntax_tree_view),
@@ -37,6 +76,13 @@ pub fn init(cx: &mut App) {
cx,
)
});
+ workspace.register_action(|workspace, _: &UseActiveEditor, window, cx| {
+ if let Some(tree_view) = workspace.item_of_type::<SyntaxTreeView>(cx) {
+ tree_view.update(cx, |view, cx| {
+ view.update_active_editor(&Default::default(), window, cx)
+ })
+ }
+ });
})
.detach();
}
@@ -45,6 +91,9 @@ pub struct SyntaxTreeView {
workspace_handle: WeakEntity<Workspace>,
editor: Option<EditorState>,
list_scroll_handle: UniformListScrollHandle,
+ /// The last active editor in the workspace. Note that this is specifically not the
+ /// currently shown editor.
+ last_active_editor: Option<Entity<Editor>>,
selected_descendant_ix: Option<usize>,
hovered_descendant_ix: Option<usize>,
focus_handle: FocusHandle,
@@ -61,6 +110,14 @@ struct EditorState {
_subscription: gpui::Subscription,
}
+impl EditorState {
+ fn has_language(&self) -> bool {
+ self.active_buffer
+ .as_ref()
+ .is_some_and(|buffer| buffer.active_layer.is_some())
+ }
+}
+
#[derive(Clone)]
struct BufferState {
buffer: Entity<Buffer>,
@@ -79,17 +136,25 @@ impl SyntaxTreeView {
workspace_handle: workspace_handle.clone(),
list_scroll_handle: UniformListScrollHandle::new(),
editor: None,
+ last_active_editor: None,
hovered_descendant_ix: None,
selected_descendant_ix: None,
focus_handle: cx.focus_handle(),
};
- this.workspace_updated(active_item, window, cx);
- cx.observe_in(
+ this.handle_item_updated(active_item, window, cx);
+
+ cx.subscribe_in(
&workspace_handle.upgrade().unwrap(),
window,
- |this, workspace, window, cx| {
- this.workspace_updated(workspace.read(cx).active_item(cx), window, cx);
+ move |this, workspace, event, window, cx| match event {
+ WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => {
+ this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx)
+ }
+ WorkspaceEvent::ItemRemoved { item_id } => {
+ this.handle_item_removed(item_id, window, cx);
+ }
+ _ => {}
},
)
.detach();
@@ -97,20 +162,56 @@ impl SyntaxTreeView {
this
}
- fn workspace_updated(
+ fn handle_item_updated(
&mut self,
active_item: Option<Box<dyn ItemHandle>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(item) = active_item
- && item.item_id() != cx.entity_id()
- && let Some(editor) = item.act_as::<Editor>(cx)
- {
+ let Some(editor) = active_item
+ .filter(|item| item.item_id() != cx.entity_id())
+ .and_then(|item| item.act_as::<Editor>(cx))
+ else {
+ return;
+ };
+
+ if let Some(editor_state) = self.editor.as_ref().filter(|state| state.has_language()) {
+ self.last_active_editor = (editor_state.editor != editor).then_some(editor);
+ } else {
self.set_editor(editor, window, cx);
}
}
+ fn handle_item_removed(
+ &mut self,
+ item_id: &EntityId,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self
+ .editor
+ .as_ref()
+ .is_some_and(|state| state.editor.entity_id() == *item_id)
+ {
+ self.editor = None;
+ // Try activating the last active editor if there is one
+ self.update_active_editor(&Default::default(), window, cx);
+ cx.notify();
+ }
+ }
+
+ fn update_active_editor(
+ &mut self,
+ _: &UseActiveEditor,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(editor) = self.last_active_editor.take() else {
+ return;
+ };
+ self.set_editor(editor, window, cx);
+ }
+
fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
if let Some(state) = &self.editor {
if state.editor == editor {
@@ -294,101 +395,153 @@ impl SyntaxTreeView {
.pl(rems(depth as f32))
.hover(|style| style.bg(colors.element_hover))
}
-}
-
-impl Render for SyntaxTreeView {
- fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let mut rendered = div().flex_1().bg(cx.theme().colors().editor_background);
- if let Some(layer) = self
- .editor
- .as_ref()
- .and_then(|editor| editor.active_buffer.as_ref())
- .and_then(|buffer| buffer.active_layer.as_ref())
- {
- let layer = layer.clone();
- rendered = rendered.child(uniform_list(
- "SyntaxTreeView",
- layer.node().descendant_count(),
- cx.processor(move |this, range: Range<usize>, _, cx| {
- let mut items = Vec::new();
- let mut cursor = layer.node().walk();
- let mut descendant_ix = range.start;
- cursor.goto_descendant(descendant_ix);
- let mut depth = cursor.depth();
- let mut visited_children = false;
- while descendant_ix < range.end {
- if visited_children {
- if cursor.goto_next_sibling() {
- visited_children = false;
- } else if cursor.goto_parent() {
- depth -= 1;
- } else {
- break;
- }
- } else {
- items.push(
- Self::render_node(
- &cursor,
- depth,
- Some(descendant_ix) == this.selected_descendant_ix,
+ fn compute_items(
+ &mut self,
+ layer: &OwnedSyntaxLayer,
+ range: Range<usize>,
+ cx: &Context<Self>,
+ ) -> Vec<Div> {
+ let mut items = Vec::new();
+ let mut cursor = layer.node().walk();
+ let mut descendant_ix = range.start;
+ cursor.goto_descendant(descendant_ix);
+ let mut depth = cursor.depth();
+ let mut visited_children = false;
+ while descendant_ix < range.end {
+ if visited_children {
+ if cursor.goto_next_sibling() {
+ visited_children = false;
+ } else if cursor.goto_parent() {
+ depth -= 1;
+ } else {
+ break;
+ }
+ } else {
+ items.push(
+ Self::render_node(
+ &cursor,
+ depth,
+ Some(descendant_ix) == self.selected_descendant_ix,
+ cx,
+ )
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
+ tree_view.update_editor_with_range_for_descendant_ix(
+ descendant_ix,
+ window,
+ cx,
+ |editor, mut range, window, cx| {
+ // Put the cursor at the beginning of the node.
+ mem::swap(&mut range.start, &mut range.end);
+
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |selections| {
+ selections.select_ranges(vec![range]);
+ },
+ );
+ },
+ );
+ }),
+ )
+ .on_mouse_move(cx.listener(
+ move |tree_view, _: &MouseMoveEvent, window, cx| {
+ if tree_view.hovered_descendant_ix != Some(descendant_ix) {
+ tree_view.hovered_descendant_ix = Some(descendant_ix);
+ tree_view.update_editor_with_range_for_descendant_ix(
+ descendant_ix,
+ window,
cx,
- )
- .on_mouse_down(
- MouseButton::Left,
- cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
- tree_view.update_editor_with_range_for_descendant_ix(
- descendant_ix,
- window, cx,
- |editor, mut range, window, cx| {
- // Put the cursor at the beginning of the node.
- mem::swap(&mut range.start, &mut range.end);
-
- editor.change_selections(
- SelectionEffects::scroll(Autoscroll::newest()),
- window, cx,
- |selections| {
- selections.select_ranges(vec![range]);
- },
- );
+ |editor, range, _, cx| {
+ editor.clear_background_highlights::<Self>(cx);
+ editor.highlight_background::<Self>(
+ &[range],
+ |theme| {
+ theme
+ .colors()
+ .editor_document_highlight_write_background
},
+ cx,
);
- }),
- )
- .on_mouse_move(cx.listener(
- move |tree_view, _: &MouseMoveEvent, window, cx| {
- if tree_view.hovered_descendant_ix != Some(descendant_ix) {
- tree_view.hovered_descendant_ix = Some(descendant_ix);
- tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, window, cx, |editor, range, _, cx| {
- editor.clear_background_highlights::<Self>( cx);
- editor.highlight_background::<Self>(
- &[range],
- |theme| theme.colors().editor_document_highlight_write_background,
- cx,
- );
- });
- cx.notify();
- }
},
- )),
- );
- descendant_ix += 1;
- if cursor.goto_first_child() {
- depth += 1;
- } else {
- visited_children = true;
+ );
+ cx.notify();
}
- }
- }
- items
- }),
- )
- .size_full()
- .track_scroll(self.list_scroll_handle.clone())
- .text_bg(cx.theme().colors().background).into_any_element());
+ },
+ )),
+ );
+ descendant_ix += 1;
+ if cursor.goto_first_child() {
+ depth += 1;
+ } else {
+ visited_children = true;
+ }
+ }
}
+ items
+ }
+}
- rendered
+impl Render for SyntaxTreeView {
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .flex_1()
+ .bg(cx.theme().colors().editor_background)
+ .map(|this| {
+ let editor_state = self.editor.as_ref();
+
+ if let Some(layer) = editor_state
+ .and_then(|editor| editor.active_buffer.as_ref())
+ .and_then(|buffer| buffer.active_layer.as_ref())
+ {
+ let layer = layer.clone();
+ this.child(
+ uniform_list(
+ "SyntaxTreeView",
+ layer.node().descendant_count(),
+ cx.processor(move |this, range: Range<usize>, _, cx| {
+ this.compute_items(&layer, range, cx)
+ }),
+ )
+ .size_full()
+ .track_scroll(self.list_scroll_handle.clone())
+ .text_bg(cx.theme().colors().background)
+ .into_any_element(),
+ )
+ } else {
+ let inner_content = v_flex()
+ .items_center()
+ .text_center()
+ .gap_2()
+ .max_w_3_5()
+ .map(|this| {
+ if editor_state.is_some_and(|state| !state.has_language()) {
+ this.child(Label::new("Current editor has no associated language"))
+ .child(
+ Label::new(concat!(
+ "Try assigning a language or",
+ "switching to a different buffer"
+ ))
+ .size(LabelSize::Small),
+ )
+ } else {
+ this.child(Label::new("Not attached to an editor")).child(
+ Label::new("Focus an editor to show a new tree view")
+ .size(LabelSize::Small),
+ )
+ }
+ });
+
+ this.h_flex()
+ .size_full()
+ .justify_center()
+ .child(inner_content)
+ }
+ })
}
}
@@ -506,6 +659,26 @@ impl SyntaxTreeToolbarItemView {
.child(Label::new(active_layer.language.name()))
.child(Label::new(format_node_range(active_layer.node())))
}
+
+ fn render_update_button(&mut self, cx: &mut Context<Self>) -> Option<IconButton> {
+ self.tree_view.as_ref().and_then(|view| {
+ view.update(cx, |view, cx| {
+ view.last_active_editor.as_ref().map(|editor| {
+ IconButton::new("syntax-view-update", IconName::RotateCw)
+ .tooltip({
+ let active_tab_name = editor.read_with(cx, |editor, cx| {
+ editor.tab_content_text(Default::default(), cx)
+ });
+
+ Tooltip::text(format!("Update view to '{active_tab_name}'"))
+ })
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.update_active_editor(&Default::default(), window, cx);
+ }))
+ })
+ })
+ })
+ }
}
fn format_node_range(node: Node) -> String {
@@ -522,8 +695,10 @@ fn format_node_range(node: Node) -> String {
impl Render for SyntaxTreeToolbarItemView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- self.render_menu(cx)
- .unwrap_or_else(|| PopoverMenu::new("Empty Syntax Tree"))
+ h_flex()
+ .gap_1()
+ .children(self.render_menu(cx))
+ .children(self.render_update_button(cx))
}
}
@@ -70,7 +70,7 @@ pub fn init(cx: &mut App) {
move |is_enabled, _workspace, _, cx| {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
- filter.show_action_types(settings_ui_actions.iter());
+ filter.show_action_types(&settings_ui_actions);
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
@@ -1031,6 +1031,9 @@ pub enum Event {
item: Box<dyn ItemHandle>,
},
ActiveItemChanged,
+ ItemRemoved {
+ item_id: EntityId,
+ },
UserSavedItem {
pane: WeakEntity<Pane>,
item: Box<dyn WeakItemHandle>,
@@ -3945,6 +3948,9 @@ impl Workspace {
{
entry.remove();
}
+ cx.emit(Event::ItemRemoved {
+ item_id: item.item_id(),
+ });
}
pane::Event::Focus => {
window.invalidate_character_coordinates();
@@ -4502,6 +4502,7 @@ mod tests {
"snippets",
"supermaven",
"svg",
+ "syntax_tree_view",
"tab_switcher",
"task",
"terminal",
@@ -4511,11 +4512,11 @@ mod tests {
"toolchain",
"variable_list",
"vim",
+ "window",
"workspace",
"zed",
"zed_predict_onboarding",
"zeta",
- "window",
];
assert_eq!(
all_namespaces,
@@ -86,7 +86,7 @@ fn feature_gate_predict_edits_actions(cx: &mut App) {
if is_ai_disabled {
filter.hide_action_types(&zeta_all_action_types);
} else if has_feature_flag {
- filter.show_action_types(rate_completion_action_types.iter());
+ filter.show_action_types(&rate_completion_action_types);
} else {
filter.hide_action_types(&rate_completion_action_types);
}
@@ -98,7 +98,7 @@ fn feature_gate_predict_edits_actions(cx: &mut App) {
if !DisableAiSettings::get_global(cx).disable_ai {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
- filter.show_action_types(rate_completion_action_types.iter());
+ filter.show_action_types(&rate_completion_action_types);
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {