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