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