1use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
2use gpui::{
3 App, AppContext as _, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, Hsla,
4 InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement,
5 Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle, WeakEntity, Window,
6 actions, div, rems, uniform_list,
7};
8use language::{Buffer, OwnedSyntaxLayer};
9use std::{mem, ops::Range};
10use theme::ActiveTheme;
11use tree_sitter::{Node, TreeCursor};
12use ui::{ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex};
13use workspace::{
14 SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
15 item::{Item, ItemHandle},
16};
17
18actions!(
19 dev,
20 [
21 /// Opens the syntax tree view for the current file.
22 OpenSyntaxTreeView
23 ]
24);
25
26pub fn init(cx: &mut App) {
27 cx.observe_new(|workspace: &mut Workspace, _, _| {
28 workspace.register_action(|workspace, _: &OpenSyntaxTreeView, window, cx| {
29 let active_item = workspace.active_item(cx);
30 let workspace_handle = workspace.weak_handle();
31 let syntax_tree_view =
32 cx.new(|cx| SyntaxTreeView::new(workspace_handle, active_item, window, cx));
33 workspace.split_item(
34 SplitDirection::Right,
35 Box::new(syntax_tree_view),
36 window,
37 cx,
38 )
39 });
40 })
41 .detach();
42}
43
44pub struct SyntaxTreeView {
45 workspace_handle: WeakEntity<Workspace>,
46 editor: Option<EditorState>,
47 list_scroll_handle: UniformListScrollHandle,
48 selected_descendant_ix: Option<usize>,
49 hovered_descendant_ix: Option<usize>,
50 focus_handle: FocusHandle,
51}
52
53pub struct SyntaxTreeToolbarItemView {
54 tree_view: Option<Entity<SyntaxTreeView>>,
55 subscription: Option<gpui::Subscription>,
56}
57
58struct EditorState {
59 editor: Entity<Editor>,
60 active_buffer: Option<BufferState>,
61 _subscription: gpui::Subscription,
62}
63
64#[derive(Clone)]
65struct BufferState {
66 buffer: Entity<Buffer>,
67 excerpt_id: ExcerptId,
68 active_layer: Option<OwnedSyntaxLayer>,
69}
70
71impl SyntaxTreeView {
72 pub fn new(
73 workspace_handle: WeakEntity<Workspace>,
74 active_item: Option<Box<dyn ItemHandle>>,
75 window: &mut Window,
76 cx: &mut Context<Self>,
77 ) -> Self {
78 let mut this = Self {
79 workspace_handle: workspace_handle.clone(),
80 list_scroll_handle: UniformListScrollHandle::new(),
81 editor: None,
82 hovered_descendant_ix: None,
83 selected_descendant_ix: None,
84 focus_handle: cx.focus_handle(),
85 };
86
87 this.workspace_updated(active_item, window, cx);
88 cx.observe_in(
89 &workspace_handle.upgrade().unwrap(),
90 window,
91 |this, workspace, window, cx| {
92 this.workspace_updated(workspace.read(cx).active_item(cx), window, cx);
93 },
94 )
95 .detach();
96
97 this
98 }
99
100 fn workspace_updated(
101 &mut self,
102 active_item: Option<Box<dyn ItemHandle>>,
103 window: &mut Window,
104 cx: &mut Context<Self>,
105 ) {
106 if let Some(item) = active_item
107 && item.item_id() != cx.entity_id()
108 && let Some(editor) = item.act_as::<Editor>(cx)
109 {
110 self.set_editor(editor, window, cx);
111 }
112 }
113
114 fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
115 if let Some(state) = &self.editor {
116 if state.editor == editor {
117 return;
118 }
119 editor.update(cx, |editor, cx| {
120 editor.clear_background_highlights::<Self>(cx)
121 });
122 }
123
124 let subscription = cx.subscribe_in(&editor, window, |this, _, event, window, cx| {
125 let did_reparse = match event {
126 editor::EditorEvent::Reparsed(_) => true,
127 editor::EditorEvent::SelectionsChanged { .. } => false,
128 _ => return,
129 };
130 this.editor_updated(did_reparse, window, cx);
131 });
132
133 self.editor = Some(EditorState {
134 editor,
135 _subscription: subscription,
136 active_buffer: None,
137 });
138 self.editor_updated(true, window, cx);
139 }
140
141 fn editor_updated(
142 &mut self,
143 did_reparse: bool,
144 window: &mut Window,
145 cx: &mut Context<Self>,
146 ) -> Option<()> {
147 // Find which excerpt the cursor is in, and the position within that excerpted buffer.
148 let editor_state = self.editor.as_mut()?;
149 let snapshot = editor_state
150 .editor
151 .update(cx, |editor, cx| editor.snapshot(window, cx));
152 let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| {
153 let selection_range = editor.selections.last::<usize>(cx).range();
154 let multi_buffer = editor.buffer().read(cx);
155 let (buffer, range, excerpt_id) = snapshot
156 .buffer_snapshot
157 .range_to_buffer_ranges(selection_range)
158 .pop()?;
159 let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap();
160 Some((buffer, range, excerpt_id))
161 })?;
162
163 // If the cursor has moved into a different excerpt, retrieve a new syntax layer
164 // from that buffer.
165 let buffer_state = editor_state
166 .active_buffer
167 .get_or_insert_with(|| BufferState {
168 buffer: buffer.clone(),
169 excerpt_id,
170 active_layer: None,
171 });
172 let mut prev_layer = None;
173 if did_reparse {
174 prev_layer = buffer_state.active_layer.take();
175 }
176 if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt_id {
177 buffer_state.buffer = buffer.clone();
178 buffer_state.excerpt_id = excerpt_id;
179 buffer_state.active_layer = None;
180 }
181
182 let layer = match &mut buffer_state.active_layer {
183 Some(layer) => layer,
184 None => {
185 let snapshot = buffer.read(cx).snapshot();
186 let layer = if let Some(prev_layer) = prev_layer {
187 let prev_range = prev_layer.node().byte_range();
188 snapshot
189 .syntax_layers()
190 .filter(|layer| layer.language == &prev_layer.language)
191 .min_by_key(|layer| {
192 let range = layer.node().byte_range();
193 ((range.start as i64) - (prev_range.start as i64)).abs()
194 + ((range.end as i64) - (prev_range.end as i64)).abs()
195 })?
196 } else {
197 snapshot.syntax_layers().next()?
198 };
199 buffer_state.active_layer.insert(layer.to_owned())
200 }
201 };
202
203 // Within the active layer, find the syntax node under the cursor,
204 // and scroll to it.
205 let mut cursor = layer.node().walk();
206 while cursor.goto_first_child_for_byte(range.start).is_some() {
207 if !range.is_empty() && cursor.node().end_byte() == range.start {
208 cursor.goto_next_sibling();
209 }
210 }
211
212 // Ascend to the smallest ancestor that contains the range.
213 loop {
214 let node_range = cursor.node().byte_range();
215 if node_range.start <= range.start && node_range.end >= range.end {
216 break;
217 }
218 if !cursor.goto_parent() {
219 break;
220 }
221 }
222
223 let descendant_ix = cursor.descendant_index();
224 self.selected_descendant_ix = Some(descendant_ix);
225 self.list_scroll_handle
226 .scroll_to_item(descendant_ix, ScrollStrategy::Center);
227
228 cx.notify();
229 Some(())
230 }
231
232 fn update_editor_with_range_for_descendant_ix(
233 &self,
234 descendant_ix: usize,
235 window: &mut Window,
236 cx: &mut Context<Self>,
237 mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut Window, &mut Context<Editor>),
238 ) -> Option<()> {
239 let editor_state = self.editor.as_ref()?;
240 let buffer_state = editor_state.active_buffer.as_ref()?;
241 let layer = buffer_state.active_layer.as_ref()?;
242
243 // Find the node.
244 let mut cursor = layer.node().walk();
245 cursor.goto_descendant(descendant_ix);
246 let node = cursor.node();
247 let range = node.byte_range();
248
249 // Build a text anchor range.
250 let buffer = buffer_state.buffer.read(cx);
251 let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
252
253 // Build a multibuffer anchor range.
254 let multibuffer = editor_state.editor.read(cx).buffer();
255 let multibuffer = multibuffer.read(cx).snapshot(cx);
256 let excerpt_id = buffer_state.excerpt_id;
257 let range = multibuffer
258 .anchor_in_excerpt(excerpt_id, range.start)
259 .unwrap()
260 ..multibuffer
261 .anchor_in_excerpt(excerpt_id, range.end)
262 .unwrap();
263
264 // Update the editor with the anchor range.
265 editor_state.editor.update(cx, |editor, cx| {
266 f(editor, range, window, cx);
267 });
268 Some(())
269 }
270
271 fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &App) -> Div {
272 let colors = cx.theme().colors();
273 let mut row = h_flex();
274 if let Some(field_name) = cursor.field_name() {
275 row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]);
276 }
277
278 let node = cursor.node();
279 row.child(if node.is_named() {
280 Label::new(node.kind()).color(Color::Default)
281 } else {
282 Label::new(format!("\"{}\"", node.kind())).color(Color::Created)
283 })
284 .child(
285 div()
286 .child(Label::new(format_node_range(node)).color(Color::Muted))
287 .pl_1(),
288 )
289 .text_bg(if selected {
290 colors.element_selected
291 } else {
292 Hsla::default()
293 })
294 .pl(rems(depth as f32))
295 .hover(|style| style.bg(colors.element_hover))
296 }
297}
298
299impl Render for SyntaxTreeView {
300 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
301 let mut rendered = div().flex_1().bg(cx.theme().colors().editor_background);
302
303 if let Some(layer) = self
304 .editor
305 .as_ref()
306 .and_then(|editor| editor.active_buffer.as_ref())
307 .and_then(|buffer| buffer.active_layer.as_ref())
308 {
309 let layer = layer.clone();
310 rendered = rendered.child(uniform_list(
311 "SyntaxTreeView",
312 layer.node().descendant_count(),
313 cx.processor(move |this, range: Range<usize>, _, cx| {
314 let mut items = Vec::new();
315 let mut cursor = layer.node().walk();
316 let mut descendant_ix = range.start;
317 cursor.goto_descendant(descendant_ix);
318 let mut depth = cursor.depth();
319 let mut visited_children = false;
320 while descendant_ix < range.end {
321 if visited_children {
322 if cursor.goto_next_sibling() {
323 visited_children = false;
324 } else if cursor.goto_parent() {
325 depth -= 1;
326 } else {
327 break;
328 }
329 } else {
330 items.push(
331 Self::render_node(
332 &cursor,
333 depth,
334 Some(descendant_ix) == this.selected_descendant_ix,
335 cx,
336 )
337 .on_mouse_down(
338 MouseButton::Left,
339 cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
340 tree_view.update_editor_with_range_for_descendant_ix(
341 descendant_ix,
342 window, cx,
343 |editor, mut range, window, cx| {
344 // Put the cursor at the beginning of the node.
345 mem::swap(&mut range.start, &mut range.end);
346
347 editor.change_selections(
348 SelectionEffects::scroll(Autoscroll::newest()),
349 window, cx,
350 |selections| {
351 selections.select_ranges(vec![range]);
352 },
353 );
354 },
355 );
356 }),
357 )
358 .on_mouse_move(cx.listener(
359 move |tree_view, _: &MouseMoveEvent, window, cx| {
360 if tree_view.hovered_descendant_ix != Some(descendant_ix) {
361 tree_view.hovered_descendant_ix = Some(descendant_ix);
362 tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, window, cx, |editor, range, _, cx| {
363 editor.clear_background_highlights::<Self>( cx);
364 editor.highlight_background::<Self>(
365 &[range],
366 |theme| theme.colors().editor_document_highlight_write_background,
367 cx,
368 );
369 });
370 cx.notify();
371 }
372 },
373 )),
374 );
375 descendant_ix += 1;
376 if cursor.goto_first_child() {
377 depth += 1;
378 } else {
379 visited_children = true;
380 }
381 }
382 }
383 items
384 }),
385 )
386 .size_full()
387 .track_scroll(self.list_scroll_handle.clone())
388 .text_bg(cx.theme().colors().background).into_any_element());
389 }
390
391 rendered
392 }
393}
394
395impl EventEmitter<()> for SyntaxTreeView {}
396
397impl Focusable for SyntaxTreeView {
398 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
399 self.focus_handle.clone()
400 }
401}
402
403impl Item for SyntaxTreeView {
404 type Event = ();
405
406 fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
407
408 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
409 "Syntax Tree".into()
410 }
411
412 fn telemetry_event_text(&self) -> Option<&'static str> {
413 None
414 }
415
416 fn clone_on_split(
417 &self,
418 _: Option<workspace::WorkspaceId>,
419 window: &mut Window,
420 cx: &mut Context<Self>,
421 ) -> Option<Entity<Self>>
422 where
423 Self: Sized,
424 {
425 Some(cx.new(|cx| {
426 let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
427 if let Some(editor) = &self.editor {
428 clone.set_editor(editor.editor.clone(), window, cx)
429 }
430 clone
431 }))
432 }
433}
434
435impl Default for SyntaxTreeToolbarItemView {
436 fn default() -> Self {
437 Self::new()
438 }
439}
440
441impl SyntaxTreeToolbarItemView {
442 pub fn new() -> Self {
443 Self {
444 tree_view: None,
445 subscription: None,
446 }
447 }
448
449 fn render_menu(&mut self, cx: &mut Context<Self>) -> Option<PopoverMenu<ContextMenu>> {
450 let tree_view = self.tree_view.as_ref()?;
451 let tree_view = tree_view.read(cx);
452
453 let editor_state = tree_view.editor.as_ref()?;
454 let buffer_state = editor_state.active_buffer.as_ref()?;
455 let active_layer = buffer_state.active_layer.clone()?;
456 let active_buffer = buffer_state.buffer.read(cx).snapshot();
457
458 let view = cx.entity();
459 Some(
460 PopoverMenu::new("Syntax Tree")
461 .trigger(Self::render_header(&active_layer))
462 .menu(move |window, cx| {
463 ContextMenu::build(window, cx, |mut menu, window, _| {
464 for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() {
465 menu = menu.entry(
466 format!(
467 "{} {}",
468 layer.language.name(),
469 format_node_range(layer.node())
470 ),
471 None,
472 window.handler_for(&view, move |view, window, cx| {
473 view.select_layer(layer_ix, window, cx);
474 }),
475 );
476 }
477 menu
478 })
479 .into()
480 }),
481 )
482 }
483
484 fn select_layer(
485 &mut self,
486 layer_ix: usize,
487 window: &mut Window,
488 cx: &mut Context<Self>,
489 ) -> Option<()> {
490 let tree_view = self.tree_view.as_ref()?;
491 tree_view.update(cx, |view, cx| {
492 let editor_state = view.editor.as_mut()?;
493 let buffer_state = editor_state.active_buffer.as_mut()?;
494 let snapshot = buffer_state.buffer.read(cx).snapshot();
495 let layer = snapshot.syntax_layers().nth(layer_ix)?;
496 buffer_state.active_layer = Some(layer.to_owned());
497 view.selected_descendant_ix = None;
498 cx.notify();
499 view.focus_handle.focus(window);
500 Some(())
501 })
502 }
503
504 fn render_header(active_layer: &OwnedSyntaxLayer) -> ButtonLike {
505 ButtonLike::new("syntax tree header")
506 .child(Label::new(active_layer.language.name()))
507 .child(Label::new(format_node_range(active_layer.node())))
508 }
509}
510
511fn format_node_range(node: Node) -> String {
512 let start = node.start_position();
513 let end = node.end_position();
514 format!(
515 "[{}:{} - {}:{}]",
516 start.row + 1,
517 start.column + 1,
518 end.row + 1,
519 end.column + 1,
520 )
521}
522
523impl Render for SyntaxTreeToolbarItemView {
524 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
525 self.render_menu(cx)
526 .unwrap_or_else(|| PopoverMenu::new("Empty Syntax Tree"))
527 }
528}
529
530impl EventEmitter<ToolbarItemEvent> for SyntaxTreeToolbarItemView {}
531
532impl ToolbarItemView for SyntaxTreeToolbarItemView {
533 fn set_active_pane_item(
534 &mut self,
535 active_pane_item: Option<&dyn ItemHandle>,
536 window: &mut Window,
537 cx: &mut Context<Self>,
538 ) -> ToolbarItemLocation {
539 if let Some(item) = active_pane_item
540 && let Some(view) = item.downcast::<SyntaxTreeView>()
541 {
542 self.tree_view = Some(view.clone());
543 self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
544 return ToolbarItemLocation::PrimaryLeft;
545 }
546 self.tree_view = None;
547 self.subscription = None;
548 ToolbarItemLocation::Hidden
549 }
550}