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