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 soft_wrap: false,
377 };
378
379 let line_height = cx.font_cache().line_height(font_size);
380 if Some(line_height) != self.line_height {
381 self.line_height = Some(line_height);
382 self.hover_state_changed(cx);
383 }
384
385 if let Some(layer) = self
386 .editor
387 .as_ref()
388 .and_then(|editor| editor.active_buffer.as_ref())
389 .and_then(|buffer| buffer.active_layer.as_ref())
390 {
391 let layer = layer.clone();
392 let theme = editor_theme.clone();
393 return MouseEventHandler::new::<Self, _>(0, cx, move |state, cx| {
394 let list_hovered = state.hovered();
395 UniformList::new(
396 self.list_state.clone(),
397 layer.node().descendant_count(),
398 cx,
399 move |this, range, items, cx| {
400 let mut cursor = layer.node().walk();
401 let mut descendant_ix = range.start as usize;
402 cursor.goto_descendant(descendant_ix);
403 let mut depth = cursor.depth();
404 let mut visited_children = false;
405 while descendant_ix < range.end {
406 if visited_children {
407 if cursor.goto_next_sibling() {
408 visited_children = false;
409 } else if cursor.goto_parent() {
410 depth -= 1;
411 } else {
412 break;
413 }
414 } else {
415 items.push(Self::render_node(
416 &cursor,
417 depth,
418 Some(descendant_ix) == this.selected_descendant_ix,
419 Some(descendant_ix) == this.hovered_descendant_ix,
420 list_hovered,
421 &style,
422 &theme,
423 cx,
424 ));
425 descendant_ix += 1;
426 if cursor.goto_first_child() {
427 depth += 1;
428 } else {
429 visited_children = true;
430 }
431 }
432 }
433 },
434 )
435 })
436 .on_move(move |event, this, cx| {
437 let y = event.position.y() - event.region.origin_y();
438 this.mouse_y = Some(y);
439 this.hover_state_changed(cx);
440 })
441 .on_click(MouseButton::Left, move |event, this, cx| {
442 let y = event.position.y() - event.region.origin_y();
443 this.handle_click(y, cx);
444 })
445 .contained()
446 .with_background_color(editor_theme.background)
447 .into_any();
448 }
449
450 Empty::new().into_any()
451 }
452}
453
454impl Item for SyntaxTreeView {
455 fn tab_content<V: 'static>(
456 &self,
457 _: Option<usize>,
458 style: &theme::Tab,
459 _: &AppContext,
460 ) -> gpui::AnyElement<V> {
461 Label::new("Syntax Tree", style.label.clone()).into_any()
462 }
463
464 fn clone_on_split(
465 &self,
466 _workspace_id: workspace::WorkspaceId,
467 cx: &mut ViewContext<Self>,
468 ) -> Option<Self>
469 where
470 Self: Sized,
471 {
472 let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
473 if let Some(editor) = &self.editor {
474 clone.set_editor(editor.editor.clone(), cx)
475 }
476 Some(clone)
477 }
478}
479
480impl SyntaxTreeToolbarItemView {
481 pub fn new() -> Self {
482 Self {
483 menu_open: false,
484 tree_view: None,
485 subscription: None,
486 }
487 }
488
489 fn render_menu(
490 &mut self,
491 cx: &mut ViewContext<'_, '_, Self>,
492 ) -> Option<gpui::AnyElement<Self>> {
493 let theme = theme::current(cx).clone();
494 let tree_view = self.tree_view.as_ref()?;
495 let tree_view = tree_view.read(cx);
496
497 let editor_state = tree_view.editor.as_ref()?;
498 let buffer_state = editor_state.active_buffer.as_ref()?;
499 let active_layer = buffer_state.active_layer.clone()?;
500 let active_buffer = buffer_state.buffer.read(cx).snapshot();
501
502 enum Menu {}
503
504 Some(
505 Stack::new()
506 .with_child(Self::render_header(&theme, &active_layer, cx))
507 .with_children(self.menu_open.then(|| {
508 Overlay::new(
509 MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
510 Flex::column()
511 .with_children(active_buffer.syntax_layers().enumerate().map(
512 |(ix, layer)| {
513 Self::render_menu_item(&theme, &active_layer, layer, ix, cx)
514 },
515 ))
516 .contained()
517 .with_style(theme.toolbar_dropdown_menu.container)
518 .constrained()
519 .with_width(400.)
520 .with_height(400.)
521 })
522 .on_down_out(MouseButton::Left, |_, this, cx| {
523 this.menu_open = false;
524 cx.notify()
525 }),
526 )
527 .with_hoverable(true)
528 .with_fit_mode(OverlayFitMode::SwitchAnchor)
529 .with_anchor_corner(AnchorCorner::TopLeft)
530 .with_z_index(999)
531 .aligned()
532 .bottom()
533 .left()
534 }))
535 .aligned()
536 .left()
537 .clipped()
538 .into_any(),
539 )
540 }
541
542 fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
543 self.menu_open = !self.menu_open;
544 cx.notify();
545 }
546
547 fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
548 let tree_view = self.tree_view.as_ref()?;
549 tree_view.update(cx, |view, cx| {
550 let editor_state = view.editor.as_mut()?;
551 let buffer_state = editor_state.active_buffer.as_mut()?;
552 let snapshot = buffer_state.buffer.read(cx).snapshot();
553 let layer = snapshot.syntax_layers().nth(layer_ix)?;
554 buffer_state.active_layer = Some(layer.to_owned());
555 view.selected_descendant_ix = None;
556 self.menu_open = false;
557 cx.notify();
558 Some(())
559 })
560 }
561
562 fn render_header(
563 theme: &Arc<Theme>,
564 active_layer: &OwnedSyntaxLayerInfo,
565 cx: &mut ViewContext<Self>,
566 ) -> impl Element<Self> {
567 enum ToggleMenu {}
568 MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, _| {
569 let style = theme.toolbar_dropdown_menu.header.style_for(state);
570 Flex::row()
571 .with_child(
572 Label::new(active_layer.language.name().to_string(), style.text.clone())
573 .contained()
574 .with_margin_right(style.secondary_text_spacing),
575 )
576 .with_child(Label::new(
577 format_node_range(active_layer.node()),
578 style
579 .secondary_text
580 .clone()
581 .unwrap_or_else(|| style.text.clone()),
582 ))
583 .contained()
584 .with_style(style.container)
585 })
586 .with_cursor_style(CursorStyle::PointingHand)
587 .on_click(MouseButton::Left, move |_, view, cx| {
588 view.toggle_menu(cx);
589 })
590 }
591
592 fn render_menu_item(
593 theme: &Arc<Theme>,
594 active_layer: &OwnedSyntaxLayerInfo,
595 layer: SyntaxLayerInfo,
596 layer_ix: usize,
597 cx: &mut ViewContext<Self>,
598 ) -> impl Element<Self> {
599 enum ActivateLayer {}
600 MouseEventHandler::new::<ActivateLayer, _>(layer_ix, cx, move |state, _| {
601 let is_selected = layer.node() == active_layer.node();
602 let style = theme
603 .toolbar_dropdown_menu
604 .item
605 .in_state(is_selected)
606 .style_for(state);
607 Flex::row()
608 .with_child(
609 Label::new(layer.language.name().to_string(), style.text.clone())
610 .contained()
611 .with_margin_right(style.secondary_text_spacing),
612 )
613 .with_child(Label::new(
614 format_node_range(layer.node()),
615 style
616 .secondary_text
617 .clone()
618 .unwrap_or_else(|| style.text.clone()),
619 ))
620 .contained()
621 .with_style(style.container)
622 })
623 .with_cursor_style(CursorStyle::PointingHand)
624 .on_click(MouseButton::Left, move |_, view, cx| {
625 view.select_layer(layer_ix, cx);
626 })
627 }
628}
629
630fn format_node_range(node: Node) -> String {
631 let start = node.start_position();
632 let end = node.end_position();
633 format!(
634 "[{}:{} - {}:{}]",
635 start.row + 1,
636 start.column + 1,
637 end.row + 1,
638 end.column + 1,
639 )
640}
641
642impl Entity for SyntaxTreeToolbarItemView {
643 type Event = ();
644}
645
646impl View for SyntaxTreeToolbarItemView {
647 fn ui_name() -> &'static str {
648 "SyntaxTreeToolbarItemView"
649 }
650
651 fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
652 self.render_menu(cx)
653 .unwrap_or_else(|| Empty::new().into_any())
654 }
655}
656
657impl ToolbarItemView for SyntaxTreeToolbarItemView {
658 fn set_active_pane_item(
659 &mut self,
660 active_pane_item: Option<&dyn ItemHandle>,
661 cx: &mut ViewContext<Self>,
662 ) -> workspace::ToolbarItemLocation {
663 self.menu_open = false;
664 if let Some(item) = active_pane_item {
665 if let Some(view) = item.downcast::<SyntaxTreeView>() {
666 self.tree_view = Some(view.clone());
667 self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
668 return ToolbarItemLocation::PrimaryLeft {
669 flex: Some((1., false)),
670 };
671 }
672 }
673 self.tree_view = None;
674 self.subscription = None;
675 ToolbarItemLocation::Hidden
676 }
677}