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