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