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::{ops::Range, sync::Arc};
14use theme::{Theme, ThemeSettings};
15use tree_sitter::Node;
16use workspace::{
17 item::{Item, ItemHandle},
18 ToolbarItemLocation, ToolbarItemView, Workspace,
19};
20
21actions!(log, [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, range, cx| {
216 editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
217 selections.select_ranges(vec![range]);
218 });
219 });
220 Some(())
221 }
222
223 fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) {
224 if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) {
225 let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
226 if self.hovered_descendant_ix != Some(ix) {
227 self.hovered_descendant_ix = Some(ix);
228 self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
229 editor.clear_background_highlights::<Self>(cx);
230 editor.highlight_background::<Self>(
231 vec![range],
232 |theme| theme.editor.document_highlight_write_background,
233 cx,
234 );
235 });
236 cx.notify();
237 }
238 }
239 }
240
241 fn update_editor_with_range_for_descendant_ix(
242 &self,
243 descendant_ix: usize,
244 cx: &mut ViewContext<Self>,
245 mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>),
246 ) -> Option<()> {
247 let editor_state = self.editor.as_ref()?;
248 let buffer_state = editor_state.active_buffer.as_ref()?;
249 let layer = buffer_state.active_layer.as_ref()?;
250
251 // Find the node.
252 let mut cursor = layer.node().walk();
253 cursor.goto_descendant(descendant_ix);
254 let node = cursor.node();
255 let range = node.byte_range();
256
257 // Build a text anchor range.
258 let buffer = buffer_state.buffer.read(cx);
259 let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
260
261 // Build a multibuffer anchor range.
262 let multibuffer = editor_state.editor.read(cx).buffer();
263 let multibuffer = multibuffer.read(cx).snapshot(cx);
264 let excerpt_id = buffer_state.excerpt_id;
265 let range = multibuffer.anchor_in_excerpt(excerpt_id, range.start)
266 ..multibuffer.anchor_in_excerpt(excerpt_id, range.end);
267
268 // Update the editor with the anchor range.
269 editor_state.editor.update(cx, |editor, cx| {
270 f(editor, range, cx);
271 });
272 Some(())
273 }
274
275 fn render_node(
276 node: Node,
277 depth: u32,
278 selected: bool,
279 hovered: bool,
280 list_hovered: bool,
281 style: &TextStyle,
282 editor_theme: &theme::Editor,
283 cx: &AppContext,
284 ) -> gpui::AnyElement<SyntaxTreeView> {
285 let mut range_style = style.clone();
286 let mut anonymous_node_style = style.clone();
287 let em_width = style.em_width(cx.font_cache());
288 let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round();
289
290 range_style.color = editor_theme.line_number;
291
292 let string_color = editor_theme
293 .syntax
294 .highlights
295 .iter()
296 .find_map(|(name, style)| (name == "string").then(|| style.color)?);
297 if let Some(color) = string_color {
298 anonymous_node_style.color = color;
299 }
300
301 Flex::row()
302 .with_child(
303 if node.is_named() {
304 Label::new(node.kind(), style.clone())
305 } else {
306 Label::new(format!("\"{}\"", node.kind()), anonymous_node_style)
307 }
308 .contained()
309 .with_margin_right(em_width),
310 )
311 .with_child(Label::new(format_node_range(node), range_style))
312 .contained()
313 .with_background_color(if selected {
314 editor_theme.selection.selection
315 } else if hovered && list_hovered {
316 editor_theme.active_line_background
317 } else {
318 Default::default()
319 })
320 .with_padding_left(gutter_padding + depth as f32 * 18.0)
321 .into_any()
322 }
323}
324
325impl Entity for SyntaxTreeView {
326 type Event = ();
327}
328
329impl View for SyntaxTreeView {
330 fn ui_name() -> &'static str {
331 "SyntaxTreeView"
332 }
333
334 fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
335 let settings = settings::get::<ThemeSettings>(cx);
336 let font_family_id = settings.buffer_font_family;
337 let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
338 let font_properties = Default::default();
339 let font_id = cx
340 .font_cache()
341 .select_font(font_family_id, &font_properties)
342 .unwrap();
343 let font_size = settings.buffer_font_size(cx);
344
345 let editor_theme = settings.theme.editor.clone();
346 let style = TextStyle {
347 color: editor_theme.text_color,
348 font_family_name,
349 font_family_id,
350 font_id,
351 font_size,
352 font_properties: Default::default(),
353 underline: Default::default(),
354 };
355
356 let line_height = cx.font_cache().line_height(font_size);
357 if Some(line_height) != self.line_height {
358 self.line_height = Some(line_height);
359 self.hover_state_changed(cx);
360 }
361
362 if let Some(layer) = self
363 .editor
364 .as_ref()
365 .and_then(|editor| editor.active_buffer.as_ref())
366 .and_then(|buffer| buffer.active_layer.as_ref())
367 {
368 let layer = layer.clone();
369 let theme = editor_theme.clone();
370 return MouseEventHandler::<Self, Self>::new(0, cx, move |state, cx| {
371 let list_hovered = state.hovered();
372 UniformList::new(
373 self.list_state.clone(),
374 layer.node().descendant_count(),
375 cx,
376 move |this, range, items, cx| {
377 let mut cursor = layer.node().walk();
378 let mut descendant_ix = range.start as usize;
379 cursor.goto_descendant(descendant_ix);
380 let mut depth = cursor.depth();
381 let mut visited_children = false;
382 while descendant_ix < range.end {
383 if visited_children {
384 if cursor.goto_next_sibling() {
385 visited_children = false;
386 } else if cursor.goto_parent() {
387 depth -= 1;
388 } else {
389 break;
390 }
391 } else {
392 items.push(Self::render_node(
393 cursor.node(),
394 depth,
395 Some(descendant_ix) == this.selected_descendant_ix,
396 Some(descendant_ix) == this.hovered_descendant_ix,
397 list_hovered,
398 &style,
399 &theme,
400 cx,
401 ));
402 descendant_ix += 1;
403 if cursor.goto_first_child() {
404 depth += 1;
405 } else {
406 visited_children = true;
407 }
408 }
409 }
410 },
411 )
412 })
413 .on_move(move |event, this, cx| {
414 let y = event.position.y() - event.region.origin_y();
415 this.mouse_y = Some(y);
416 this.hover_state_changed(cx);
417 })
418 .on_click(MouseButton::Left, move |event, this, cx| {
419 let y = event.position.y() - event.region.origin_y();
420 this.handle_click(y, cx);
421 })
422 .contained()
423 .with_background_color(editor_theme.background)
424 .into_any();
425 }
426
427 Empty::new().into_any()
428 }
429}
430
431impl Item for SyntaxTreeView {
432 fn tab_content<V: View>(
433 &self,
434 _: Option<usize>,
435 style: &theme::Tab,
436 _: &AppContext,
437 ) -> gpui::AnyElement<V> {
438 Label::new("Syntax Tree", style.label.clone()).into_any()
439 }
440
441 fn clone_on_split(
442 &self,
443 _workspace_id: workspace::WorkspaceId,
444 cx: &mut ViewContext<Self>,
445 ) -> Option<Self>
446 where
447 Self: Sized,
448 {
449 let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
450 if let Some(editor) = &self.editor {
451 clone.set_editor(editor.editor.clone(), cx)
452 }
453 Some(clone)
454 }
455}
456
457impl SyntaxTreeToolbarItemView {
458 pub fn new() -> Self {
459 Self {
460 menu_open: false,
461 tree_view: None,
462 subscription: None,
463 }
464 }
465
466 fn render_menu(
467 &mut self,
468 cx: &mut ViewContext<'_, '_, Self>,
469 ) -> Option<gpui::AnyElement<Self>> {
470 let theme = theme::current(cx).clone();
471 let tree_view = self.tree_view.as_ref()?;
472 let tree_view = tree_view.read(cx);
473
474 let editor_state = tree_view.editor.as_ref()?;
475 let buffer_state = editor_state.active_buffer.as_ref()?;
476 let active_layer = buffer_state.active_layer.clone()?;
477 let active_buffer = buffer_state.buffer.read(cx).snapshot();
478
479 enum Menu {}
480
481 Some(
482 Stack::new()
483 .with_child(Self::render_header(&theme, &active_layer, cx))
484 .with_children(self.menu_open.then(|| {
485 Overlay::new(
486 MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
487 Flex::column()
488 .with_children(active_buffer.syntax_layers().enumerate().map(
489 |(ix, layer)| {
490 Self::render_menu_item(&theme, &active_layer, layer, ix, cx)
491 },
492 ))
493 .contained()
494 .with_style(theme.toolbar_dropdown_menu.container)
495 .constrained()
496 .with_width(400.)
497 .with_height(400.)
498 })
499 .on_down_out(MouseButton::Left, |_, this, cx| {
500 this.menu_open = false;
501 cx.notify()
502 }),
503 )
504 .with_hoverable(true)
505 .with_fit_mode(OverlayFitMode::SwitchAnchor)
506 .with_anchor_corner(AnchorCorner::TopLeft)
507 .with_z_index(999)
508 .aligned()
509 .bottom()
510 .left()
511 }))
512 .aligned()
513 .left()
514 .clipped()
515 .into_any(),
516 )
517 }
518
519 fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
520 self.menu_open = !self.menu_open;
521 cx.notify();
522 }
523
524 fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
525 let tree_view = self.tree_view.as_ref()?;
526 tree_view.update(cx, |view, cx| {
527 let editor_state = view.editor.as_mut()?;
528 let buffer_state = editor_state.active_buffer.as_mut()?;
529 let snapshot = buffer_state.buffer.read(cx).snapshot();
530 let layer = snapshot.syntax_layers().nth(layer_ix)?;
531 buffer_state.active_layer = Some(layer.to_owned());
532 view.selected_descendant_ix = None;
533 self.menu_open = false;
534 cx.notify();
535 Some(())
536 })
537 }
538
539 fn render_header(
540 theme: &Arc<Theme>,
541 active_layer: &OwnedSyntaxLayerInfo,
542 cx: &mut ViewContext<Self>,
543 ) -> impl Element<Self> {
544 enum ToggleMenu {}
545 MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
546 let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
547 Flex::row()
548 .with_child(
549 Label::new(active_layer.language.name().to_string(), style.text.clone())
550 .contained()
551 .with_margin_right(style.secondary_text_spacing),
552 )
553 .with_child(Label::new(
554 format_node_range(active_layer.node()),
555 style
556 .secondary_text
557 .clone()
558 .unwrap_or_else(|| style.text.clone()),
559 ))
560 .contained()
561 .with_style(style.container)
562 })
563 .with_cursor_style(CursorStyle::PointingHand)
564 .on_click(MouseButton::Left, move |_, view, cx| {
565 view.toggle_menu(cx);
566 })
567 }
568
569 fn render_menu_item(
570 theme: &Arc<Theme>,
571 active_layer: &OwnedSyntaxLayerInfo,
572 layer: SyntaxLayerInfo,
573 layer_ix: usize,
574 cx: &mut ViewContext<Self>,
575 ) -> impl Element<Self> {
576 enum ActivateLayer {}
577 MouseEventHandler::<ActivateLayer, _>::new(layer_ix, cx, move |state, _| {
578 let is_selected = layer.node() == active_layer.node();
579 let style = theme
580 .toolbar_dropdown_menu
581 .item
582 .style_for(state, is_selected);
583 Flex::row()
584 .with_child(
585 Label::new(layer.language.name().to_string(), style.text.clone())
586 .contained()
587 .with_margin_right(style.secondary_text_spacing),
588 )
589 .with_child(Label::new(
590 format_node_range(layer.node()),
591 style
592 .secondary_text
593 .clone()
594 .unwrap_or_else(|| style.text.clone()),
595 ))
596 .contained()
597 .with_style(style.container)
598 })
599 .with_cursor_style(CursorStyle::PointingHand)
600 .on_click(MouseButton::Left, move |_, view, cx| {
601 view.select_layer(layer_ix, cx);
602 })
603 }
604}
605
606fn format_node_range(node: Node) -> String {
607 let start = node.start_position();
608 let end = node.end_position();
609 format!(
610 "[{}:{} - {}:{}]",
611 start.row + 1,
612 start.column + 1,
613 end.row + 1,
614 end.column + 1,
615 )
616}
617
618impl Entity for SyntaxTreeToolbarItemView {
619 type Event = ();
620}
621
622impl View for SyntaxTreeToolbarItemView {
623 fn ui_name() -> &'static str {
624 "SyntaxTreeToolbarItemView"
625 }
626
627 fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
628 self.render_menu(cx)
629 .unwrap_or_else(|| Empty::new().into_any())
630 }
631}
632
633impl ToolbarItemView for SyntaxTreeToolbarItemView {
634 fn set_active_pane_item(
635 &mut self,
636 active_pane_item: Option<&dyn ItemHandle>,
637 cx: &mut ViewContext<Self>,
638 ) -> workspace::ToolbarItemLocation {
639 self.menu_open = false;
640 if let Some(item) = active_pane_item {
641 if let Some(view) = item.downcast::<SyntaxTreeView>() {
642 self.tree_view = Some(view.clone());
643 self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
644 return ToolbarItemLocation::PrimaryLeft {
645 flex: Some((1., false)),
646 };
647 }
648 }
649 self.tree_view = None;
650 self.subscription = None;
651 ToolbarItemLocation::Hidden
652 }
653}