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