1use command_palette_hooks::CommandPaletteFilter;
2use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
3use gpui::{
4 App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
5 Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
6 ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle,
7 WeakEntity, Window, actions, div, rems, uniform_list,
8};
9use language::{Buffer, OwnedSyntaxLayer};
10use std::{any::TypeId, mem, ops::Range};
11use theme::ActiveTheme;
12use tree_sitter::{Node, TreeCursor};
13use ui::{
14 ButtonCommon, ButtonLike, Clickable, Color, ContextMenu, FluentBuilder as _, IconButton,
15 IconName, Label, LabelCommon, LabelSize, PopoverMenu, StyledExt, Tooltip, WithScrollbar,
16 h_flex, v_flex,
17};
18use workspace::{
19 Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation,
20 ToolbarItemView, Workspace,
21 item::{Item, ItemHandle},
22};
23
24actions!(
25 dev,
26 [
27 /// Opens the syntax tree view for the current file.
28 OpenSyntaxTreeView,
29 ]
30);
31
32actions!(
33 syntax_tree_view,
34 [
35 /// Update the syntax tree view to show the last focused file.
36 UseActiveEditor
37 ]
38);
39
40pub fn init(cx: &mut App) {
41 let syntax_tree_actions = [TypeId::of::<UseActiveEditor>()];
42
43 CommandPaletteFilter::update_global(cx, |this, _| {
44 this.hide_action_types(&syntax_tree_actions);
45 });
46
47 cx.observe_new(move |workspace: &mut Workspace, _, _| {
48 workspace.register_action(move |workspace, _: &OpenSyntaxTreeView, window, cx| {
49 CommandPaletteFilter::update_global(cx, |this, _| {
50 this.show_action_types(&syntax_tree_actions);
51 });
52
53 let active_item = workspace.active_item(cx);
54 let workspace_handle = workspace.weak_handle();
55 let syntax_tree_view = cx.new(|cx| {
56 cx.on_release(move |view: &mut SyntaxTreeView, cx| {
57 if view
58 .workspace_handle
59 .read_with(cx, |workspace, cx| {
60 workspace.item_of_type::<SyntaxTreeView>(cx).is_none()
61 })
62 .unwrap_or_default()
63 {
64 CommandPaletteFilter::update_global(cx, |this, _| {
65 this.hide_action_types(&syntax_tree_actions);
66 });
67 }
68 })
69 .detach();
70
71 SyntaxTreeView::new(workspace_handle, active_item, window, cx)
72 });
73 workspace.split_item(
74 SplitDirection::Right,
75 Box::new(syntax_tree_view),
76 window,
77 cx,
78 )
79 });
80 workspace.register_action(|workspace, _: &UseActiveEditor, window, cx| {
81 if let Some(tree_view) = workspace.item_of_type::<SyntaxTreeView>(cx) {
82 tree_view.update(cx, |view, cx| {
83 view.update_active_editor(&Default::default(), window, cx)
84 })
85 }
86 });
87 })
88 .detach();
89}
90
91pub struct SyntaxTreeView {
92 workspace_handle: WeakEntity<Workspace>,
93 editor: Option<EditorState>,
94 list_scroll_handle: UniformListScrollHandle,
95 /// The last active editor in the workspace. Note that this is specifically not the
96 /// currently shown editor.
97 last_active_editor: Option<Entity<Editor>>,
98 selected_descendant_ix: Option<usize>,
99 hovered_descendant_ix: Option<usize>,
100 focus_handle: FocusHandle,
101}
102
103pub struct SyntaxTreeToolbarItemView {
104 tree_view: Option<Entity<SyntaxTreeView>>,
105 subscription: Option<gpui::Subscription>,
106}
107
108struct EditorState {
109 editor: Entity<Editor>,
110 active_buffer: Option<BufferState>,
111 _subscription: gpui::Subscription,
112}
113
114impl EditorState {
115 fn has_language(&self) -> bool {
116 self.active_buffer
117 .as_ref()
118 .is_some_and(|buffer| buffer.active_layer.is_some())
119 }
120}
121
122#[derive(Clone)]
123struct BufferState {
124 buffer: Entity<Buffer>,
125 excerpt_id: ExcerptId,
126 active_layer: Option<OwnedSyntaxLayer>,
127}
128
129impl SyntaxTreeView {
130 pub fn new(
131 workspace_handle: WeakEntity<Workspace>,
132 active_item: Option<Box<dyn ItemHandle>>,
133 window: &mut Window,
134 cx: &mut Context<Self>,
135 ) -> Self {
136 let mut this = Self {
137 workspace_handle: workspace_handle.clone(),
138 list_scroll_handle: UniformListScrollHandle::new(),
139 editor: None,
140 last_active_editor: None,
141 hovered_descendant_ix: None,
142 selected_descendant_ix: None,
143 focus_handle: cx.focus_handle(),
144 };
145
146 this.handle_item_updated(active_item, window, cx);
147
148 cx.subscribe_in(
149 &workspace_handle.upgrade().unwrap(),
150 window,
151 move |this, workspace, event, window, cx| match event {
152 WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => {
153 this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx)
154 }
155 WorkspaceEvent::ItemRemoved { item_id } => {
156 this.handle_item_removed(item_id, window, cx);
157 }
158 _ => {}
159 },
160 )
161 .detach();
162
163 this
164 }
165
166 fn handle_item_updated(
167 &mut self,
168 active_item: Option<Box<dyn ItemHandle>>,
169 window: &mut Window,
170 cx: &mut Context<Self>,
171 ) {
172 let Some(editor) = active_item
173 .filter(|item| item.item_id() != cx.entity_id())
174 .and_then(|item| item.act_as::<Editor>(cx))
175 else {
176 return;
177 };
178
179 if let Some(editor_state) = self.editor.as_ref().filter(|state| state.has_language()) {
180 self.last_active_editor = (editor_state.editor != editor).then_some(editor);
181 } else {
182 self.set_editor(editor, window, cx);
183 }
184 }
185
186 fn handle_item_removed(
187 &mut self,
188 item_id: &EntityId,
189 window: &mut Window,
190 cx: &mut Context<Self>,
191 ) {
192 if self
193 .editor
194 .as_ref()
195 .is_some_and(|state| state.editor.entity_id() == *item_id)
196 {
197 self.editor = None;
198 // Try activating the last active editor if there is one
199 self.update_active_editor(&Default::default(), window, cx);
200 cx.notify();
201 }
202 }
203
204 fn update_active_editor(
205 &mut self,
206 _: &UseActiveEditor,
207 window: &mut Window,
208 cx: &mut Context<Self>,
209 ) {
210 let Some(editor) = self.last_active_editor.take() else {
211 return;
212 };
213 self.set_editor(editor, window, cx);
214 }
215
216 fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
217 if let Some(state) = &self.editor {
218 if state.editor == editor {
219 return;
220 }
221 editor.update(cx, |editor, cx| {
222 editor.clear_background_highlights::<Self>(cx)
223 });
224 }
225
226 let subscription = cx.subscribe_in(&editor, window, |this, _, event, window, cx| {
227 let did_reparse = match event {
228 editor::EditorEvent::Reparsed(_) => true,
229 editor::EditorEvent::SelectionsChanged { .. } => false,
230 _ => return,
231 };
232 this.editor_updated(did_reparse, window, cx);
233 });
234
235 self.editor = Some(EditorState {
236 editor,
237 _subscription: subscription,
238 active_buffer: None,
239 });
240 self.editor_updated(true, window, cx);
241 }
242
243 fn editor_updated(
244 &mut self,
245 did_reparse: bool,
246 window: &mut Window,
247 cx: &mut Context<Self>,
248 ) -> Option<()> {
249 // Find which excerpt the cursor is in, and the position within that excerpted buffer.
250 let editor_state = self.editor.as_mut()?;
251 let snapshot = editor_state
252 .editor
253 .update(cx, |editor, cx| editor.snapshot(window, cx));
254 let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| {
255 let selection_range = editor.selections.last::<usize>(cx).range();
256 let multi_buffer = editor.buffer().read(cx);
257 let (buffer, range, excerpt_id) = snapshot
258 .buffer_snapshot
259 .range_to_buffer_ranges(selection_range)
260 .pop()?;
261 let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap();
262 Some((buffer, range, excerpt_id))
263 })?;
264
265 // If the cursor has moved into a different excerpt, retrieve a new syntax layer
266 // from that buffer.
267 let buffer_state = editor_state
268 .active_buffer
269 .get_or_insert_with(|| BufferState {
270 buffer: buffer.clone(),
271 excerpt_id,
272 active_layer: None,
273 });
274 let mut prev_layer = None;
275 if did_reparse {
276 prev_layer = buffer_state.active_layer.take();
277 }
278 if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt_id {
279 buffer_state.buffer = buffer.clone();
280 buffer_state.excerpt_id = excerpt_id;
281 buffer_state.active_layer = None;
282 }
283
284 let layer = match &mut buffer_state.active_layer {
285 Some(layer) => layer,
286 None => {
287 let snapshot = buffer.read(cx).snapshot();
288 let layer = if let Some(prev_layer) = prev_layer {
289 let prev_range = prev_layer.node().byte_range();
290 snapshot
291 .syntax_layers()
292 .filter(|layer| layer.language == &prev_layer.language)
293 .min_by_key(|layer| {
294 let range = layer.node().byte_range();
295 ((range.start as i64) - (prev_range.start as i64)).abs()
296 + ((range.end as i64) - (prev_range.end as i64)).abs()
297 })?
298 } else {
299 snapshot.syntax_layers().next()?
300 };
301 buffer_state.active_layer.insert(layer.to_owned())
302 }
303 };
304
305 // Within the active layer, find the syntax node under the cursor,
306 // and scroll to it.
307 let mut cursor = layer.node().walk();
308 while cursor.goto_first_child_for_byte(range.start).is_some() {
309 if !range.is_empty() && cursor.node().end_byte() == range.start {
310 cursor.goto_next_sibling();
311 }
312 }
313
314 // Ascend to the smallest ancestor that contains the range.
315 loop {
316 let node_range = cursor.node().byte_range();
317 if node_range.start <= range.start && node_range.end >= range.end {
318 break;
319 }
320 if !cursor.goto_parent() {
321 break;
322 }
323 }
324
325 let descendant_ix = cursor.descendant_index();
326 self.selected_descendant_ix = Some(descendant_ix);
327 self.list_scroll_handle
328 .scroll_to_item(descendant_ix, ScrollStrategy::Center);
329
330 cx.notify();
331 Some(())
332 }
333
334 fn update_editor_with_range_for_descendant_ix(
335 &self,
336 descendant_ix: usize,
337 window: &mut Window,
338 cx: &mut Context<Self>,
339 mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut Window, &mut Context<Editor>),
340 ) -> Option<()> {
341 let editor_state = self.editor.as_ref()?;
342 let buffer_state = editor_state.active_buffer.as_ref()?;
343 let layer = buffer_state.active_layer.as_ref()?;
344
345 // Find the node.
346 let mut cursor = layer.node().walk();
347 cursor.goto_descendant(descendant_ix);
348 let node = cursor.node();
349 let range = node.byte_range();
350
351 // Build a text anchor range.
352 let buffer = buffer_state.buffer.read(cx);
353 let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
354
355 // Build a multibuffer anchor range.
356 let multibuffer = editor_state.editor.read(cx).buffer();
357 let multibuffer = multibuffer.read(cx).snapshot(cx);
358 let excerpt_id = buffer_state.excerpt_id;
359 let range = multibuffer
360 .anchor_in_excerpt(excerpt_id, range.start)
361 .unwrap()
362 ..multibuffer
363 .anchor_in_excerpt(excerpt_id, range.end)
364 .unwrap();
365
366 // Update the editor with the anchor range.
367 editor_state.editor.update(cx, |editor, cx| {
368 f(editor, range, window, cx);
369 });
370 Some(())
371 }
372
373 fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &App) -> Div {
374 let colors = cx.theme().colors();
375 let mut row = h_flex();
376 if let Some(field_name) = cursor.field_name() {
377 row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]);
378 }
379
380 let node = cursor.node();
381 row.child(if node.is_named() {
382 Label::new(node.kind()).color(Color::Default)
383 } else {
384 Label::new(format!("\"{}\"", node.kind())).color(Color::Created)
385 })
386 .child(
387 div()
388 .child(Label::new(format_node_range(node)).color(Color::Muted))
389 .pl_1(),
390 )
391 .text_bg(if selected {
392 colors.element_selected
393 } else {
394 Hsla::default()
395 })
396 .pl(rems(depth as f32))
397 .hover(|style| style.bg(colors.element_hover))
398 }
399
400 fn compute_items(
401 &mut self,
402 layer: &OwnedSyntaxLayer,
403 range: Range<usize>,
404 cx: &Context<Self>,
405 ) -> Vec<Div> {
406 let mut items = Vec::new();
407 let mut cursor = layer.node().walk();
408 let mut descendant_ix = range.start;
409 cursor.goto_descendant(descendant_ix);
410 let mut depth = cursor.depth();
411 let mut visited_children = false;
412 while descendant_ix < range.end {
413 if visited_children {
414 if cursor.goto_next_sibling() {
415 visited_children = false;
416 } else if cursor.goto_parent() {
417 depth -= 1;
418 } else {
419 break;
420 }
421 } else {
422 items.push(
423 Self::render_node(
424 &cursor,
425 depth,
426 Some(descendant_ix) == self.selected_descendant_ix,
427 cx,
428 )
429 .on_mouse_down(
430 MouseButton::Left,
431 cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
432 tree_view.update_editor_with_range_for_descendant_ix(
433 descendant_ix,
434 window,
435 cx,
436 |editor, mut range, window, cx| {
437 // Put the cursor at the beginning of the node.
438 mem::swap(&mut range.start, &mut range.end);
439
440 editor.change_selections(
441 SelectionEffects::scroll(Autoscroll::newest()),
442 window,
443 cx,
444 |selections| {
445 selections.select_ranges(vec![range]);
446 },
447 );
448 },
449 );
450 }),
451 )
452 .on_mouse_move(cx.listener(
453 move |tree_view, _: &MouseMoveEvent, window, cx| {
454 if tree_view.hovered_descendant_ix != Some(descendant_ix) {
455 tree_view.hovered_descendant_ix = Some(descendant_ix);
456 tree_view.update_editor_with_range_for_descendant_ix(
457 descendant_ix,
458 window,
459 cx,
460 |editor, range, _, cx| {
461 editor.clear_background_highlights::<Self>(cx);
462 editor.highlight_background::<Self>(
463 &[range],
464 |theme| {
465 theme
466 .colors()
467 .editor_document_highlight_write_background
468 },
469 cx,
470 );
471 },
472 );
473 cx.notify();
474 }
475 },
476 )),
477 );
478 descendant_ix += 1;
479 if cursor.goto_first_child() {
480 depth += 1;
481 } else {
482 visited_children = true;
483 }
484 }
485 }
486 items
487 }
488}
489
490impl Render for SyntaxTreeView {
491 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
492 div()
493 .flex_1()
494 .bg(cx.theme().colors().editor_background)
495 .map(|this| {
496 let editor_state = self.editor.as_ref();
497
498 if let Some(layer) = editor_state
499 .and_then(|editor| editor.active_buffer.as_ref())
500 .and_then(|buffer| buffer.active_layer.as_ref())
501 {
502 let layer = layer.clone();
503 this.child(
504 uniform_list(
505 "SyntaxTreeView",
506 layer.node().descendant_count(),
507 cx.processor(move |this, range: Range<usize>, _, cx| {
508 this.compute_items(&layer, range, cx)
509 }),
510 )
511 .size_full()
512 .track_scroll(self.list_scroll_handle.clone())
513 .text_bg(cx.theme().colors().background)
514 .into_any_element(),
515 )
516 .vertical_scrollbar_for(self.list_scroll_handle.clone(), window, cx)
517 .into_any_element()
518 } else {
519 let inner_content = v_flex()
520 .items_center()
521 .text_center()
522 .gap_2()
523 .max_w_3_5()
524 .map(|this| {
525 if editor_state.is_some_and(|state| !state.has_language()) {
526 this.child(Label::new("Current editor has no associated language"))
527 .child(
528 Label::new(concat!(
529 "Try assigning a language or",
530 "switching to a different buffer"
531 ))
532 .size(LabelSize::Small),
533 )
534 } else {
535 this.child(Label::new("Not attached to an editor")).child(
536 Label::new("Focus an editor to show a new tree view")
537 .size(LabelSize::Small),
538 )
539 }
540 });
541
542 this.h_flex()
543 .size_full()
544 .justify_center()
545 .child(inner_content)
546 .into_any_element()
547 }
548 })
549 }
550}
551
552impl EventEmitter<()> for SyntaxTreeView {}
553
554impl Focusable for SyntaxTreeView {
555 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
556 self.focus_handle.clone()
557 }
558}
559
560impl Item for SyntaxTreeView {
561 type Event = ();
562
563 fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
564
565 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
566 "Syntax Tree".into()
567 }
568
569 fn telemetry_event_text(&self) -> Option<&'static str> {
570 None
571 }
572
573 fn clone_on_split(
574 &self,
575 _: Option<workspace::WorkspaceId>,
576 window: &mut Window,
577 cx: &mut Context<Self>,
578 ) -> Option<Entity<Self>>
579 where
580 Self: Sized,
581 {
582 Some(cx.new(|cx| {
583 let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
584 if let Some(editor) = &self.editor {
585 clone.set_editor(editor.editor.clone(), window, cx)
586 }
587 clone
588 }))
589 }
590}
591
592impl Default for SyntaxTreeToolbarItemView {
593 fn default() -> Self {
594 Self::new()
595 }
596}
597
598impl SyntaxTreeToolbarItemView {
599 pub fn new() -> Self {
600 Self {
601 tree_view: None,
602 subscription: None,
603 }
604 }
605
606 fn render_menu(&mut self, cx: &mut Context<Self>) -> Option<PopoverMenu<ContextMenu>> {
607 let tree_view = self.tree_view.as_ref()?;
608 let tree_view = tree_view.read(cx);
609
610 let editor_state = tree_view.editor.as_ref()?;
611 let buffer_state = editor_state.active_buffer.as_ref()?;
612 let active_layer = buffer_state.active_layer.clone()?;
613 let active_buffer = buffer_state.buffer.read(cx).snapshot();
614
615 let view = cx.entity();
616 Some(
617 PopoverMenu::new("Syntax Tree")
618 .trigger(Self::render_header(&active_layer))
619 .menu(move |window, cx| {
620 ContextMenu::build(window, cx, |mut menu, window, _| {
621 for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() {
622 menu = menu.entry(
623 format!(
624 "{} {}",
625 layer.language.name(),
626 format_node_range(layer.node())
627 ),
628 None,
629 window.handler_for(&view, move |view, window, cx| {
630 view.select_layer(layer_ix, window, cx);
631 }),
632 );
633 }
634 menu
635 })
636 .into()
637 }),
638 )
639 }
640
641 fn select_layer(
642 &mut self,
643 layer_ix: usize,
644 window: &mut Window,
645 cx: &mut Context<Self>,
646 ) -> Option<()> {
647 let tree_view = self.tree_view.as_ref()?;
648 tree_view.update(cx, |view, cx| {
649 let editor_state = view.editor.as_mut()?;
650 let buffer_state = editor_state.active_buffer.as_mut()?;
651 let snapshot = buffer_state.buffer.read(cx).snapshot();
652 let layer = snapshot.syntax_layers().nth(layer_ix)?;
653 buffer_state.active_layer = Some(layer.to_owned());
654 view.selected_descendant_ix = None;
655 cx.notify();
656 view.focus_handle.focus(window);
657 Some(())
658 })
659 }
660
661 fn render_header(active_layer: &OwnedSyntaxLayer) -> ButtonLike {
662 ButtonLike::new("syntax tree header")
663 .child(Label::new(active_layer.language.name()))
664 .child(Label::new(format_node_range(active_layer.node())))
665 }
666
667 fn render_update_button(&mut self, cx: &mut Context<Self>) -> Option<IconButton> {
668 self.tree_view.as_ref().and_then(|view| {
669 view.update(cx, |view, cx| {
670 view.last_active_editor.as_ref().map(|editor| {
671 IconButton::new("syntax-view-update", IconName::RotateCw)
672 .tooltip({
673 let active_tab_name = editor.read_with(cx, |editor, cx| {
674 editor.tab_content_text(Default::default(), cx)
675 });
676
677 Tooltip::text(format!("Update view to '{active_tab_name}'"))
678 })
679 .on_click(cx.listener(|this, _, window, cx| {
680 this.update_active_editor(&Default::default(), window, cx);
681 }))
682 })
683 })
684 })
685 }
686}
687
688fn format_node_range(node: Node) -> String {
689 let start = node.start_position();
690 let end = node.end_position();
691 format!(
692 "[{}:{} - {}:{}]",
693 start.row + 1,
694 start.column + 1,
695 end.row + 1,
696 end.column + 1,
697 )
698}
699
700impl Render for SyntaxTreeToolbarItemView {
701 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
702 h_flex()
703 .gap_1()
704 .children(self.render_menu(cx))
705 .children(self.render_update_button(cx))
706 }
707}
708
709impl EventEmitter<ToolbarItemEvent> for SyntaxTreeToolbarItemView {}
710
711impl ToolbarItemView for SyntaxTreeToolbarItemView {
712 fn set_active_pane_item(
713 &mut self,
714 active_pane_item: Option<&dyn ItemHandle>,
715 window: &mut Window,
716 cx: &mut Context<Self>,
717 ) -> ToolbarItemLocation {
718 if let Some(item) = active_pane_item
719 && let Some(view) = item.downcast::<SyntaxTreeView>()
720 {
721 self.tree_view = Some(view.clone());
722 self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
723 return ToolbarItemLocation::PrimaryLeft;
724 }
725 self.tree_view = None;
726 self.subscription = None;
727 ToolbarItemLocation::Hidden
728 }
729}