1use editor::{Anchor, Editor, ExcerptId, 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 cx.entity().clone(),
307 "SyntaxTreeView",
308 layer.node().descendant_count(),
309 move |this, range, _, cx| {
310 let mut items = Vec::new();
311 let mut cursor = layer.node().walk();
312 let mut descendant_ix = range.start;
313 cursor.goto_descendant(descendant_ix);
314 let mut depth = cursor.depth();
315 let mut visited_children = false;
316 while descendant_ix < range.end {
317 if visited_children {
318 if cursor.goto_next_sibling() {
319 visited_children = false;
320 } else if cursor.goto_parent() {
321 depth -= 1;
322 } else {
323 break;
324 }
325 } else {
326 items.push(
327 Self::render_node(
328 &cursor,
329 depth,
330 Some(descendant_ix) == this.selected_descendant_ix,
331 cx,
332 )
333 .on_mouse_down(
334 MouseButton::Left,
335 cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
336 tree_view.update_editor_with_range_for_descendant_ix(
337 descendant_ix,
338 window, cx,
339 |editor, mut range, window, cx| {
340 // Put the cursor at the beginning of the node.
341 mem::swap(&mut range.start, &mut range.end);
342
343 editor.change_selections(
344 Some(Autoscroll::newest()),
345 window, cx,
346 |selections| {
347 selections.select_ranges(vec![range]);
348 },
349 );
350 },
351 );
352 }),
353 )
354 .on_mouse_move(cx.listener(
355 move |tree_view, _: &MouseMoveEvent, window, cx| {
356 if tree_view.hovered_descendant_ix != Some(descendant_ix) {
357 tree_view.hovered_descendant_ix = Some(descendant_ix);
358 tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, window, cx, |editor, range, _, cx| {
359 editor.clear_background_highlights::<Self>( cx);
360 editor.highlight_background::<Self>(
361 &[range],
362 |theme| theme.editor_document_highlight_write_background,
363 cx,
364 );
365 });
366 cx.notify();
367 }
368 },
369 )),
370 );
371 descendant_ix += 1;
372 if cursor.goto_first_child() {
373 depth += 1;
374 } else {
375 visited_children = true;
376 }
377 }
378 }
379 items
380 },
381 )
382 .size_full()
383 .track_scroll(self.list_scroll_handle.clone())
384 .text_bg(cx.theme().colors().background).into_any_element());
385 }
386
387 rendered
388 }
389}
390
391impl EventEmitter<()> for SyntaxTreeView {}
392
393impl Focusable for SyntaxTreeView {
394 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
395 self.focus_handle.clone()
396 }
397}
398
399impl Item for SyntaxTreeView {
400 type Event = ();
401
402 fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
403
404 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
405 "Syntax Tree".into()
406 }
407
408 fn telemetry_event_text(&self) -> Option<&'static str> {
409 None
410 }
411
412 fn clone_on_split(
413 &self,
414 _: Option<workspace::WorkspaceId>,
415 window: &mut Window,
416 cx: &mut Context<Self>,
417 ) -> Option<Entity<Self>>
418 where
419 Self: Sized,
420 {
421 Some(cx.new(|cx| {
422 let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
423 if let Some(editor) = &self.editor {
424 clone.set_editor(editor.editor.clone(), window, cx)
425 }
426 clone
427 }))
428 }
429}
430
431impl Default for SyntaxTreeToolbarItemView {
432 fn default() -> Self {
433 Self::new()
434 }
435}
436
437impl SyntaxTreeToolbarItemView {
438 pub fn new() -> Self {
439 Self {
440 tree_view: None,
441 subscription: None,
442 }
443 }
444
445 fn render_menu(&mut self, cx: &mut Context<Self>) -> Option<PopoverMenu<ContextMenu>> {
446 let tree_view = self.tree_view.as_ref()?;
447 let tree_view = tree_view.read(cx);
448
449 let editor_state = tree_view.editor.as_ref()?;
450 let buffer_state = editor_state.active_buffer.as_ref()?;
451 let active_layer = buffer_state.active_layer.clone()?;
452 let active_buffer = buffer_state.buffer.read(cx).snapshot();
453
454 let view = cx.entity().clone();
455 Some(
456 PopoverMenu::new("Syntax Tree")
457 .trigger(Self::render_header(&active_layer))
458 .menu(move |window, cx| {
459 ContextMenu::build(window, cx, |mut menu, window, _| {
460 for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() {
461 menu = menu.entry(
462 format!(
463 "{} {}",
464 layer.language.name(),
465 format_node_range(layer.node())
466 ),
467 None,
468 window.handler_for(&view, move |view, window, cx| {
469 view.select_layer(layer_ix, window, cx);
470 }),
471 );
472 }
473 menu
474 })
475 .into()
476 }),
477 )
478 }
479
480 fn select_layer(
481 &mut self,
482 layer_ix: usize,
483 window: &mut Window,
484 cx: &mut Context<Self>,
485 ) -> Option<()> {
486 let tree_view = self.tree_view.as_ref()?;
487 tree_view.update(cx, |view, cx| {
488 let editor_state = view.editor.as_mut()?;
489 let buffer_state = editor_state.active_buffer.as_mut()?;
490 let snapshot = buffer_state.buffer.read(cx).snapshot();
491 let layer = snapshot.syntax_layers().nth(layer_ix)?;
492 buffer_state.active_layer = Some(layer.to_owned());
493 view.selected_descendant_ix = None;
494 cx.notify();
495 view.focus_handle.focus(window);
496 Some(())
497 })
498 }
499
500 fn render_header(active_layer: &OwnedSyntaxLayer) -> ButtonLike {
501 ButtonLike::new("syntax tree header")
502 .child(Label::new(active_layer.language.name()))
503 .child(Label::new(format_node_range(active_layer.node())))
504 }
505}
506
507fn format_node_range(node: Node) -> String {
508 let start = node.start_position();
509 let end = node.end_position();
510 format!(
511 "[{}:{} - {}:{}]",
512 start.row + 1,
513 start.column + 1,
514 end.row + 1,
515 end.column + 1,
516 )
517}
518
519impl Render for SyntaxTreeToolbarItemView {
520 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
521 self.render_menu(cx)
522 .unwrap_or_else(|| PopoverMenu::new("Empty Syntax Tree"))
523 }
524}
525
526impl EventEmitter<ToolbarItemEvent> for SyntaxTreeToolbarItemView {}
527
528impl ToolbarItemView for SyntaxTreeToolbarItemView {
529 fn set_active_pane_item(
530 &mut self,
531 active_pane_item: Option<&dyn ItemHandle>,
532 window: &mut Window,
533 cx: &mut Context<Self>,
534 ) -> ToolbarItemLocation {
535 if let Some(item) = active_pane_item {
536 if let Some(view) = item.downcast::<SyntaxTreeView>() {
537 self.tree_view = Some(view.clone());
538 self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
539 return ToolbarItemLocation::PrimaryLeft;
540 }
541 }
542 self.tree_view = None;
543 self.subscription = None;
544 ToolbarItemLocation::Hidden
545 }
546}