1use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
2use gpui::{
3 actions,
4 elements::{
5 AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
6 ParentElement, ScrollTarget, Stack, UniformList, UniformListState,
7 },
8 fonts::TextStyle,
9 platform::{CursorStyle, MouseButton},
10 AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle,
11};
12use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo};
13use std::{mem, ops::Range, sync::Arc};
14use theme::{Theme, ThemeSettings};
15use tree_sitter::{Node, TreeCursor};
16use workspace::{
17 item::{Item, ItemHandle},
18 ToolbarItemLocation, ToolbarItemView, Workspace,
19};
20
21actions!(debug, [OpenSyntaxTreeView]);
22
23pub fn init(cx: &mut AppContext) {
24 cx.add_action(
25 move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| {
26 let active_item = workspace.active_item(cx);
27 let workspace_handle = workspace.weak_handle();
28 let syntax_tree_view =
29 cx.add_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
30 workspace.add_item(Box::new(syntax_tree_view), cx);
31 },
32 );
33}
34
35pub struct SyntaxTreeView {
36 workspace_handle: WeakViewHandle<Workspace>,
37 editor: Option<EditorState>,
38 mouse_y: Option<f32>,
39 line_height: Option<f32>,
40 list_state: UniformListState,
41 selected_descendant_ix: Option<usize>,
42 hovered_descendant_ix: Option<usize>,
43}
44
45pub struct SyntaxTreeToolbarItemView {
46 tree_view: Option<ViewHandle<SyntaxTreeView>>,
47 subscription: Option<gpui::Subscription>,
48 menu_open: bool,
49}
50
51struct EditorState {
52 editor: ViewHandle<Editor>,
53 active_buffer: Option<BufferState>,
54 _subscription: gpui::Subscription,
55}
56
57#[derive(Clone)]
58struct BufferState {
59 buffer: ModelHandle<Buffer>,
60 excerpt_id: ExcerptId,
61 active_layer: Option<OwnedSyntaxLayerInfo>,
62}
63
64impl SyntaxTreeView {
65 pub fn new(
66 workspace_handle: WeakViewHandle<Workspace>,
67 active_item: Option<Box<dyn ItemHandle>>,
68 cx: &mut ViewContext<Self>,
69 ) -> Self {
70 let mut this = Self {
71 workspace_handle: workspace_handle.clone(),
72 list_state: UniformListState::default(),
73 editor: None,
74 mouse_y: None,
75 line_height: None,
76 hovered_descendant_ix: None,
77 selected_descendant_ix: None,
78 };
79
80 this.workspace_updated(active_item, cx);
81 cx.observe(
82 &workspace_handle.upgrade(cx).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.id() != cx.view_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: ViewHandle<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::Event::Reparsed => true,
119 editor::Event::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_state.scroll_to(ScrollTarget::Show(descendant_ix));
206
207 cx.notify();
208 Some(())
209 }
210
211 fn handle_click(&mut self, y: f32, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
212 let line_height = self.line_height?;
213 let ix = ((self.list_state.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_state.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(
279 cursor: &TreeCursor,
280 depth: u32,
281 selected: bool,
282 hovered: bool,
283 list_hovered: bool,
284 style: &TextStyle,
285 editor_theme: &theme::Editor,
286 cx: &AppContext,
287 ) -> gpui::AnyElement<SyntaxTreeView> {
288 let node = cursor.node();
289 let mut range_style = style.clone();
290 let em_width = style.em_width(cx.font_cache());
291 let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round();
292
293 range_style.color = editor_theme.line_number;
294
295 let mut anonymous_node_style = style.clone();
296 let string_color = editor_theme
297 .syntax
298 .highlights
299 .iter()
300 .find_map(|(name, style)| (name == "string").then(|| style.color)?);
301 let property_color = editor_theme
302 .syntax
303 .highlights
304 .iter()
305 .find_map(|(name, style)| (name == "property").then(|| style.color)?);
306 if let Some(color) = string_color {
307 anonymous_node_style.color = color;
308 }
309
310 let mut row = Flex::row();
311 if let Some(field_name) = cursor.field_name() {
312 let mut field_style = style.clone();
313 if let Some(color) = property_color {
314 field_style.color = color;
315 }
316
317 row.add_children([
318 Label::new(field_name, field_style),
319 Label::new(": ", style.clone()),
320 ]);
321 }
322
323 return row
324 .with_child(
325 if node.is_named() {
326 Label::new(node.kind(), style.clone())
327 } else {
328 Label::new(format!("\"{}\"", node.kind()), anonymous_node_style)
329 }
330 .contained()
331 .with_margin_right(em_width),
332 )
333 .with_child(Label::new(format_node_range(node), range_style))
334 .contained()
335 .with_background_color(if selected {
336 editor_theme.selection.selection
337 } else if hovered && list_hovered {
338 editor_theme.active_line_background
339 } else {
340 Default::default()
341 })
342 .with_padding_left(gutter_padding + depth as f32 * 18.0)
343 .into_any();
344 }
345}
346
347impl Entity for SyntaxTreeView {
348 type Event = ();
349}
350
351impl View for SyntaxTreeView {
352 fn ui_name() -> &'static str {
353 "SyntaxTreeView"
354 }
355
356 fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
357 let settings = settings::get::<ThemeSettings>(cx);
358 let font_family_id = settings.buffer_font_family;
359 let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
360 let font_properties = Default::default();
361 let font_id = cx
362 .font_cache()
363 .select_font(font_family_id, &font_properties)
364 .unwrap();
365 let font_size = settings.buffer_font_size(cx);
366
367 let editor_theme = settings.theme.editor.clone();
368 let style = TextStyle {
369 color: editor_theme.text_color,
370 font_family_name,
371 font_family_id,
372 font_id,
373 font_size,
374 font_properties: Default::default(),
375 underline: Default::default(),
376 };
377
378 let line_height = cx.font_cache().line_height(font_size);
379 if Some(line_height) != self.line_height {
380 self.line_height = Some(line_height);
381 self.hover_state_changed(cx);
382 }
383
384 if let Some(layer) = self
385 .editor
386 .as_ref()
387 .and_then(|editor| editor.active_buffer.as_ref())
388 .and_then(|buffer| buffer.active_layer.as_ref())
389 {
390 let layer = layer.clone();
391 let theme = editor_theme.clone();
392 return MouseEventHandler::new::<Self, _>(0, cx, move |state, cx| {
393 let list_hovered = state.hovered();
394 UniformList::new(
395 self.list_state.clone(),
396 layer.node().descendant_count(),
397 cx,
398 move |this, range, items, cx| {
399 let mut cursor = layer.node().walk();
400 let mut descendant_ix = range.start as usize;
401 cursor.goto_descendant(descendant_ix);
402 let mut depth = cursor.depth();
403 let mut visited_children = false;
404 while descendant_ix < range.end {
405 if visited_children {
406 if cursor.goto_next_sibling() {
407 visited_children = false;
408 } else if cursor.goto_parent() {
409 depth -= 1;
410 } else {
411 break;
412 }
413 } else {
414 items.push(Self::render_node(
415 &cursor,
416 depth,
417 Some(descendant_ix) == this.selected_descendant_ix,
418 Some(descendant_ix) == this.hovered_descendant_ix,
419 list_hovered,
420 &style,
421 &theme,
422 cx,
423 ));
424 descendant_ix += 1;
425 if cursor.goto_first_child() {
426 depth += 1;
427 } else {
428 visited_children = true;
429 }
430 }
431 }
432 },
433 )
434 })
435 .on_move(move |event, this, cx| {
436 let y = event.position.y() - event.region.origin_y();
437 this.mouse_y = Some(y);
438 this.hover_state_changed(cx);
439 })
440 .on_click(MouseButton::Left, move |event, this, cx| {
441 let y = event.position.y() - event.region.origin_y();
442 this.handle_click(y, cx);
443 })
444 .contained()
445 .with_background_color(editor_theme.background)
446 .into_any();
447 }
448
449 Empty::new().into_any()
450 }
451}
452
453impl Item for SyntaxTreeView {
454 fn tab_content<V: View>(
455 &self,
456 _: Option<usize>,
457 style: &theme::Tab,
458 _: &AppContext,
459 ) -> gpui::AnyElement<V> {
460 Label::new("Syntax Tree", style.label.clone()).into_any()
461 }
462
463 fn clone_on_split(
464 &self,
465 _workspace_id: workspace::WorkspaceId,
466 cx: &mut ViewContext<Self>,
467 ) -> Option<Self>
468 where
469 Self: Sized,
470 {
471 let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
472 if let Some(editor) = &self.editor {
473 clone.set_editor(editor.editor.clone(), cx)
474 }
475 Some(clone)
476 }
477}
478
479impl SyntaxTreeToolbarItemView {
480 pub fn new() -> Self {
481 Self {
482 menu_open: false,
483 tree_view: None,
484 subscription: None,
485 }
486 }
487
488 fn render_menu(
489 &mut self,
490 cx: &mut ViewContext<'_, '_, Self>,
491 ) -> Option<gpui::AnyElement<Self>> {
492 let theme = theme::current(cx).clone();
493 let tree_view = self.tree_view.as_ref()?;
494 let tree_view = tree_view.read(cx);
495
496 let editor_state = tree_view.editor.as_ref()?;
497 let buffer_state = editor_state.active_buffer.as_ref()?;
498 let active_layer = buffer_state.active_layer.clone()?;
499 let active_buffer = buffer_state.buffer.read(cx).snapshot();
500
501 enum Menu {}
502
503 Some(
504 Stack::new()
505 .with_child(Self::render_header(&theme, &active_layer, cx))
506 .with_children(self.menu_open.then(|| {
507 Overlay::new(
508 MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
509 Flex::column()
510 .with_children(active_buffer.syntax_layers().enumerate().map(
511 |(ix, layer)| {
512 Self::render_menu_item(&theme, &active_layer, layer, ix, cx)
513 },
514 ))
515 .contained()
516 .with_style(theme.toolbar_dropdown_menu.container)
517 .constrained()
518 .with_width(400.)
519 .with_height(400.)
520 })
521 .on_down_out(MouseButton::Left, |_, this, cx| {
522 this.menu_open = false;
523 cx.notify()
524 }),
525 )
526 .with_hoverable(true)
527 .with_fit_mode(OverlayFitMode::SwitchAnchor)
528 .with_anchor_corner(AnchorCorner::TopLeft)
529 .with_z_index(999)
530 .aligned()
531 .bottom()
532 .left()
533 }))
534 .aligned()
535 .left()
536 .clipped()
537 .into_any(),
538 )
539 }
540
541 fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
542 self.menu_open = !self.menu_open;
543 cx.notify();
544 }
545
546 fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
547 let tree_view = self.tree_view.as_ref()?;
548 tree_view.update(cx, |view, cx| {
549 let editor_state = view.editor.as_mut()?;
550 let buffer_state = editor_state.active_buffer.as_mut()?;
551 let snapshot = buffer_state.buffer.read(cx).snapshot();
552 let layer = snapshot.syntax_layers().nth(layer_ix)?;
553 buffer_state.active_layer = Some(layer.to_owned());
554 view.selected_descendant_ix = None;
555 self.menu_open = false;
556 cx.notify();
557 Some(())
558 })
559 }
560
561 fn render_header(
562 theme: &Arc<Theme>,
563 active_layer: &OwnedSyntaxLayerInfo,
564 cx: &mut ViewContext<Self>,
565 ) -> impl Element<Self> {
566 enum ToggleMenu {}
567 MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, _| {
568 let style = theme.toolbar_dropdown_menu.header.style_for(state);
569 Flex::row()
570 .with_child(
571 Label::new(active_layer.language.name().to_string(), style.text.clone())
572 .contained()
573 .with_margin_right(style.secondary_text_spacing),
574 )
575 .with_child(Label::new(
576 format_node_range(active_layer.node()),
577 style
578 .secondary_text
579 .clone()
580 .unwrap_or_else(|| style.text.clone()),
581 ))
582 .contained()
583 .with_style(style.container)
584 })
585 .with_cursor_style(CursorStyle::PointingHand)
586 .on_click(MouseButton::Left, move |_, view, cx| {
587 view.toggle_menu(cx);
588 })
589 }
590
591 fn render_menu_item(
592 theme: &Arc<Theme>,
593 active_layer: &OwnedSyntaxLayerInfo,
594 layer: SyntaxLayerInfo,
595 layer_ix: usize,
596 cx: &mut ViewContext<Self>,
597 ) -> impl Element<Self> {
598 enum ActivateLayer {}
599 MouseEventHandler::new::<ActivateLayer, _>(layer_ix, cx, move |state, _| {
600 let is_selected = layer.node() == active_layer.node();
601 let style = theme
602 .toolbar_dropdown_menu
603 .item
604 .in_state(is_selected)
605 .style_for(state);
606 Flex::row()
607 .with_child(
608 Label::new(layer.language.name().to_string(), style.text.clone())
609 .contained()
610 .with_margin_right(style.secondary_text_spacing),
611 )
612 .with_child(Label::new(
613 format_node_range(layer.node()),
614 style
615 .secondary_text
616 .clone()
617 .unwrap_or_else(|| style.text.clone()),
618 ))
619 .contained()
620 .with_style(style.container)
621 })
622 .with_cursor_style(CursorStyle::PointingHand)
623 .on_click(MouseButton::Left, move |_, view, cx| {
624 view.select_layer(layer_ix, cx);
625 })
626 }
627}
628
629fn format_node_range(node: Node) -> String {
630 let start = node.start_position();
631 let end = node.end_position();
632 format!(
633 "[{}:{} - {}:{}]",
634 start.row + 1,
635 start.column + 1,
636 end.row + 1,
637 end.column + 1,
638 )
639}
640
641impl Entity for SyntaxTreeToolbarItemView {
642 type Event = ();
643}
644
645impl View for SyntaxTreeToolbarItemView {
646 fn ui_name() -> &'static str {
647 "SyntaxTreeToolbarItemView"
648 }
649
650 fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
651 self.render_menu(cx)
652 .unwrap_or_else(|| Empty::new().into_any())
653 }
654}
655
656impl ToolbarItemView for SyntaxTreeToolbarItemView {
657 fn set_active_pane_item(
658 &mut self,
659 active_pane_item: Option<&dyn ItemHandle>,
660 cx: &mut ViewContext<Self>,
661 ) -> workspace::ToolbarItemLocation {
662 self.menu_open = false;
663 if let Some(item) = active_pane_item {
664 if let Some(view) = item.downcast::<SyntaxTreeView>() {
665 self.tree_view = Some(view.clone());
666 self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
667 return ToolbarItemLocation::PrimaryLeft {
668 flex: Some((1., false)),
669 };
670 }
671 }
672 self.tree_view = None;
673 self.subscription = None;
674 ToolbarItemLocation::Hidden
675 }
676}