1use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
2use gpui::{
3 actions, AnchorCorner, AppContext, CursorStyle, Div, Element, Empty, Entity, Focusable, Model,
4 MouseButton, Overlay, OverlayFitMode, ParentElement, Render, TextStyle, UniformList,
5 UniformListState, View, ViewContext, VisualContext, WeakView,
6};
7use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo};
8use std::{mem, ops::Range, sync::Arc};
9use theme::{ActiveTheme, Theme, ThemeSettings};
10use tree_sitter::{Node, TreeCursor};
11use workspace::{
12 item::{Item, ItemHandle},
13 ui::v_stack,
14 ToolbarItemLocation, ToolbarItemView, Workspace,
15};
16
17actions!(debug, [OpenSyntaxTreeView]);
18
19pub fn init(cx: &mut AppContext) {
20 cx.observe_new_views(|workspace: &mut Workspace, _| {
21 workspace.register_action(|workspace, _: &OpenSyntaxTreeView, cx| {
22 let active_item = workspace.active_item(cx);
23 let workspace_handle = workspace.weak_handle();
24 let syntax_tree_view =
25 cx.build_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
26 workspace.add_item(Box::new(syntax_tree_view), cx);
27 });
28 })
29 .detach();
30}
31
32pub struct SyntaxTreeView {
33 workspace_handle: WeakView<Workspace>,
34 editor: Option<EditorState>,
35 mouse_y: Option<f32>,
36 line_height: Option<f32>,
37 list_state: UniformListState,
38 selected_descendant_ix: Option<usize>,
39 hovered_descendant_ix: Option<usize>,
40}
41
42pub struct SyntaxTreeToolbarItemView {
43 tree_view: Option<View<SyntaxTreeView>>,
44 subscription: Option<gpui::Subscription>,
45 menu_open: bool,
46}
47
48struct EditorState {
49 editor: View<Editor>,
50 active_buffer: Option<BufferState>,
51 _subscription: gpui::Subscription,
52}
53
54#[derive(Clone)]
55struct BufferState {
56 buffer: Model<Buffer>,
57 excerpt_id: ExcerptId,
58 active_layer: Option<OwnedSyntaxLayerInfo>,
59}
60
61impl SyntaxTreeView {
62 pub fn new(
63 workspace_handle: WeakView<Workspace>,
64 active_item: Option<Box<dyn ItemHandle>>,
65 cx: &mut ViewContext<Self>,
66 ) -> Self {
67 let mut this = Self {
68 workspace_handle: workspace_handle.clone(),
69 list_state: UniformListState::default(),
70 editor: None,
71 mouse_y: None,
72 line_height: None,
73 hovered_descendant_ix: None,
74 selected_descendant_ix: None,
75 };
76
77 this.workspace_updated(active_item, cx);
78 cx.observe(
79 &workspace_handle.upgrade().unwrap(),
80 |this, workspace, cx| {
81 this.workspace_updated(workspace.read(cx).active_item(cx), cx);
82 },
83 )
84 .detach();
85
86 this
87 }
88
89 fn workspace_updated(
90 &mut self,
91 active_item: Option<Box<dyn ItemHandle>>,
92 cx: &mut ViewContext<Self>,
93 ) {
94 if let Some(item) = active_item {
95 if item.item_id() != cx.entity_id() {
96 if let Some(editor) = item.act_as::<Editor>(cx) {
97 self.set_editor(editor, cx);
98 }
99 }
100 }
101 }
102
103 fn set_editor(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
104 if let Some(state) = &self.editor {
105 if state.editor == editor {
106 return;
107 }
108 editor.update(cx, |editor, cx| {
109 editor.clear_background_highlights::<Self>(cx)
110 });
111 }
112
113 let subscription = cx.subscribe(&editor, |this, _, event, cx| {
114 let did_reparse = match event {
115 editor::EditorEvent::Reparsed => true,
116 editor::EditorEvent::SelectionsChanged { .. } => false,
117 _ => return,
118 };
119 this.editor_updated(did_reparse, cx);
120 });
121
122 self.editor = Some(EditorState {
123 editor,
124 _subscription: subscription,
125 active_buffer: None,
126 });
127 self.editor_updated(true, cx);
128 }
129
130 fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
131 // Find which excerpt the cursor is in, and the position within that excerpted buffer.
132 let editor_state = self.editor.as_mut()?;
133 let editor = &editor_state.editor.read(cx);
134 let selection_range = editor.selections.last::<usize>(cx).range();
135 let multibuffer = editor.buffer().read(cx);
136 let (buffer, range, excerpt_id) = multibuffer
137 .range_to_buffer_ranges(selection_range, cx)
138 .pop()?;
139
140 // If the cursor has moved into a different excerpt, retrieve a new syntax layer
141 // from that buffer.
142 let buffer_state = editor_state
143 .active_buffer
144 .get_or_insert_with(|| BufferState {
145 buffer: buffer.clone(),
146 excerpt_id,
147 active_layer: None,
148 });
149 let mut prev_layer = None;
150 if did_reparse {
151 prev_layer = buffer_state.active_layer.take();
152 }
153 if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id {
154 buffer_state.buffer = buffer.clone();
155 buffer_state.excerpt_id = excerpt_id;
156 buffer_state.active_layer = None;
157 }
158
159 let layer = match &mut buffer_state.active_layer {
160 Some(layer) => layer,
161 None => {
162 let snapshot = buffer.read(cx).snapshot();
163 let layer = if let Some(prev_layer) = prev_layer {
164 let prev_range = prev_layer.node().byte_range();
165 snapshot
166 .syntax_layers()
167 .filter(|layer| layer.language == &prev_layer.language)
168 .min_by_key(|layer| {
169 let range = layer.node().byte_range();
170 ((range.start as i64) - (prev_range.start as i64)).abs()
171 + ((range.end as i64) - (prev_range.end as i64)).abs()
172 })?
173 } else {
174 snapshot.syntax_layers().next()?
175 };
176 buffer_state.active_layer.insert(layer.to_owned())
177 }
178 };
179
180 // Within the active layer, find the syntax node under the cursor,
181 // and scroll to it.
182 let mut cursor = layer.node().walk();
183 while cursor.goto_first_child_for_byte(range.start).is_some() {
184 if !range.is_empty() && cursor.node().end_byte() == range.start {
185 cursor.goto_next_sibling();
186 }
187 }
188
189 // Ascend to the smallest ancestor that contains the range.
190 loop {
191 let node_range = cursor.node().byte_range();
192 if node_range.start <= range.start && node_range.end >= range.end {
193 break;
194 }
195 if !cursor.goto_parent() {
196 break;
197 }
198 }
199
200 let descendant_ix = cursor.descendant_index();
201 self.selected_descendant_ix = Some(descendant_ix);
202 self.list_state.scroll_to(ScrollTarget::Show(descendant_ix));
203
204 cx.notify();
205 Some(())
206 }
207
208 fn handle_click(&mut self, y: f32, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
209 let line_height = self.line_height?;
210 let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
211
212 self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| {
213 // Put the cursor at the beginning of the node.
214 mem::swap(&mut range.start, &mut range.end);
215
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 cursor: &TreeCursor,
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 node = cursor.node();
286 let mut range_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 mut anonymous_node_style = style.clone();
293 let string_color = editor_theme
294 .syntax
295 .highlights
296 .iter()
297 .find_map(|(name, style)| (name == "string").then(|| style.color)?);
298 let property_color = editor_theme
299 .syntax
300 .highlights
301 .iter()
302 .find_map(|(name, style)| (name == "property").then(|| style.color)?);
303 if let Some(color) = string_color {
304 anonymous_node_style.color = color;
305 }
306
307 let mut row = Flex::row();
308 if let Some(field_name) = cursor.field_name() {
309 let mut field_style = style.clone();
310 if let Some(color) = property_color {
311 field_style.color = color;
312 }
313
314 row.add_children([
315 Label::new(field_name, field_style),
316 Label::new(": ", style.clone()),
317 ]);
318 }
319
320 return row
321 .with_child(
322 if node.is_named() {
323 Label::new(node.kind(), style.clone())
324 } else {
325 Label::new(format!("\"{}\"", node.kind()), anonymous_node_style)
326 }
327 .contained()
328 .with_margin_right(em_width),
329 )
330 .with_child(Label::new(format_node_range(node), range_style))
331 .contained()
332 .with_background_color(if selected {
333 editor_theme.selection.selection
334 } else if hovered && list_hovered {
335 editor_theme.active_line_background
336 } else {
337 Default::default()
338 })
339 .with_padding_left(gutter_padding + depth as f32 * 18.0)
340 .into_any();
341 }
342}
343
344impl Entity for SyntaxTreeView {
345 type Event = ();
346}
347
348impl View for SyntaxTreeView {
349 fn ui_name() -> &'static str {
350 "SyntaxTreeView"
351 }
352
353 fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
354 let settings = settings::get::<ThemeSettings>(cx);
355 let font_family_id = settings.buffer_font_family;
356 let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
357 let font_properties = Default::default();
358 let font_id = cx
359 .font_cache()
360 .select_font(font_family_id, &font_properties)
361 .unwrap();
362 let font_size = settings.buffer_font_size(cx);
363
364 let editor_theme = settings.theme.editor.clone();
365 let style = TextStyle {
366 color: editor_theme.text_color,
367 font_family_name,
368 font_family_id,
369 font_id,
370 font_size,
371 font_properties: Default::default(),
372 underline: Default::default(),
373 soft_wrap: false,
374 };
375
376 let line_height = cx.font_cache().line_height(font_size);
377 if Some(line_height) != self.line_height {
378 self.line_height = Some(line_height);
379 self.hover_state_changed(cx);
380 }
381
382 if let Some(layer) = self
383 .editor
384 .as_ref()
385 .and_then(|editor| editor.active_buffer.as_ref())
386 .and_then(|buffer| buffer.active_layer.as_ref())
387 {
388 let layer = layer.clone();
389 let theme = editor_theme.clone();
390 return MouseEventHandler::new::<Self, _>(0, cx, move |state, cx| {
391 let list_hovered = state.hovered();
392 UniformList::new(
393 self.list_state.clone(),
394 layer.node().descendant_count(),
395 cx,
396 move |this, range, items, cx| {
397 let mut cursor = layer.node().walk();
398 let mut descendant_ix = range.start as usize;
399 cursor.goto_descendant(descendant_ix);
400 let mut depth = cursor.depth();
401 let mut visited_children = false;
402 while descendant_ix < range.end {
403 if visited_children {
404 if cursor.goto_next_sibling() {
405 visited_children = false;
406 } else if cursor.goto_parent() {
407 depth -= 1;
408 } else {
409 break;
410 }
411 } else {
412 items.push(Self::render_node(
413 &cursor,
414 depth,
415 Some(descendant_ix) == this.selected_descendant_ix,
416 Some(descendant_ix) == this.hovered_descendant_ix,
417 list_hovered,
418 &style,
419 &theme,
420 cx,
421 ));
422 descendant_ix += 1;
423 if cursor.goto_first_child() {
424 depth += 1;
425 } else {
426 visited_children = true;
427 }
428 }
429 }
430 },
431 )
432 })
433 .on_move(move |event, this, cx| {
434 let y = event.position.y() - event.region.origin_y();
435 this.mouse_y = Some(y);
436 this.hover_state_changed(cx);
437 })
438 .on_click(MouseButton::Left, move |event, this, cx| {
439 let y = event.position.y() - event.region.origin_y();
440 this.handle_click(y, cx);
441 })
442 .contained()
443 .with_background_color(editor_theme.background)
444 .into_any();
445 }
446
447 Empty::new().into_any()
448 }
449}
450
451impl Item for SyntaxTreeView {
452 fn tab_content<V: 'static>(
453 &self,
454 _: Option<usize>,
455 style: &theme::Tab,
456 _: &AppContext,
457 ) -> gpui::AnyElement<V> {
458 Label::new("Syntax Tree", style.label.clone()).into_any()
459 }
460
461 fn clone_on_split(
462 &self,
463 _workspace_id: workspace::WorkspaceId,
464 cx: &mut ViewContext<Self>,
465 ) -> Option<Self>
466 where
467 Self: Sized,
468 {
469 let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
470 if let Some(editor) = &self.editor {
471 clone.set_editor(editor.editor.clone(), cx)
472 }
473 Some(clone)
474 }
475}
476
477impl SyntaxTreeToolbarItemView {
478 pub fn new() -> Self {
479 Self {
480 menu_open: false,
481 tree_view: None,
482 subscription: None,
483 }
484 }
485
486 fn render_menu(
487 &mut self,
488 cx: &mut ViewContext<'_, '_, Self>,
489 ) -> Option<gpui::AnyElement<Self>> {
490 let theme = cx.theme().clone();
491 let tree_view = self.tree_view.as_ref()?;
492 let tree_view = tree_view.read(cx);
493
494 let editor_state = tree_view.editor.as_ref()?;
495 let buffer_state = editor_state.active_buffer.as_ref()?;
496 let active_layer = buffer_state.active_layer.clone()?;
497 let active_buffer = buffer_state.buffer.read(cx).snapshot();
498
499 enum Menu {}
500
501 Some(
502 v_stack()
503 .child(Self::render_header(&theme, &active_layer, cx))
504 .children(self.menu_open.then(|| {
505 overlay(
506 mouse_event_handler::<Menu, _>(0, cx, move |_, cx| {
507 v_stack()
508 .with_children(active_buffer.syntax_layers().enumerate().map(
509 |(ix, layer)| {
510 Self::render_menu_item(&theme, &active_layer, layer, ix, cx)
511 },
512 ))
513 .contained()
514 .with_style(theme.toolbar_dropdown_menu.container)
515 .constrained()
516 .with_width(400.)
517 .with_height(400.)
518 })
519 .on_down_out(MouseButton::Left, |_, this, cx| {
520 this.menu_open = false;
521 cx.notify()
522 }),
523 )
524 .with_hoverable(true)
525 .with_fit_content()
526 .into_any()
527 }))
528 .into_any(),
529 )
530 }
531
532 fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
533 self.menu_open = !self.menu_open;
534 cx.notify();
535 }
536
537 fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
538 let tree_view = self.tree_view.as_ref()?;
539 tree_view.update(cx, |view, cx| {
540 let editor_state = view.editor.as_mut()?;
541 let buffer_state = editor_state.active_buffer.as_mut()?;
542 let snapshot = buffer_state.buffer.read(cx).snapshot();
543 let layer = snapshot.syntax_layers().nth(layer_ix)?;
544 buffer_state.active_layer = Some(layer.to_owned());
545 view.selected_descendant_ix = None;
546 self.menu_open = false;
547 cx.notify();
548 Some(())
549 })
550 }
551
552 fn render_header(
553 theme: &Arc<Theme>,
554 active_layer: &OwnedSyntaxLayerInfo,
555 cx: &mut ViewContext<Self>,
556 ) -> impl Element<Self> {
557 enum ToggleMenu {}
558 MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, _| {
559 let style = theme.toolbar_dropdown_menu.header.style_for(state);
560 Flex::row()
561 .with_child(
562 Label::new(active_layer.language.name().to_string(), style.text.clone())
563 .contained()
564 .with_margin_right(style.secondary_text_spacing),
565 )
566 .with_child(Label::new(
567 format_node_range(active_layer.node()),
568 style
569 .secondary_text
570 .clone()
571 .unwrap_or_else(|| style.text.clone()),
572 ))
573 .contained()
574 .with_style(style.container)
575 })
576 .with_cursor_style(CursorStyle::PointingHand)
577 .on_click(MouseButton::Left, move |_, view, cx| {
578 view.toggle_menu(cx);
579 })
580 }
581
582 fn render_menu_item(
583 theme: &Arc<Theme>,
584 active_layer: &OwnedSyntaxLayerInfo,
585 layer: SyntaxLayerInfo,
586 layer_ix: usize,
587 cx: &mut ViewContext<Self>,
588 ) -> impl Element<Self> {
589 enum ActivateLayer {}
590 MouseEventHandler::new::<ActivateLayer, _>(layer_ix, cx, move |state, _| {
591 let is_selected = layer.node() == active_layer.node();
592 let style = theme
593 .toolbar_dropdown_menu
594 .item
595 .in_state(is_selected)
596 .style_for(state);
597 Flex::row()
598 .with_child(
599 Label::new(layer.language.name().to_string(), style.text.clone())
600 .contained()
601 .with_margin_right(style.secondary_text_spacing),
602 )
603 .with_child(Label::new(
604 format_node_range(layer.node()),
605 style
606 .secondary_text
607 .clone()
608 .unwrap_or_else(|| style.text.clone()),
609 ))
610 .contained()
611 .with_style(style.container)
612 })
613 .with_cursor_style(CursorStyle::PointingHand)
614 .on_click(MouseButton::Left, move |_, view, cx| {
615 view.select_layer(layer_ix, cx);
616 })
617 }
618}
619
620fn format_node_range(node: Node) -> String {
621 let start = node.start_position();
622 let end = node.end_position();
623 format!(
624 "[{}:{} - {}:{}]",
625 start.row + 1,
626 start.column + 1,
627 end.row + 1,
628 end.column + 1,
629 )
630}
631
632impl Render for SyntaxTreeToolbarItemView {
633 type Element = Focusable<Div>;
634
635 // todo!()
636 // fn ui_name() -> &'static str {
637 // "SyntaxTreeToolbarItemView"
638 // }
639
640 fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
641 self.render_menu(cx)
642 .unwrap_or_else(|| Empty::new().into_any())
643 }
644}
645
646impl ToolbarItemView for SyntaxTreeToolbarItemView {
647 fn set_active_pane_item(
648 &mut self,
649 active_pane_item: Option<&dyn ItemHandle>,
650 cx: &mut ViewContext<Self>,
651 ) -> workspace::ToolbarItemLocation {
652 self.menu_open = false;
653 if let Some(item) = active_pane_item {
654 if let Some(view) = item.downcast::<SyntaxTreeView>() {
655 self.tree_view = Some(view.clone());
656 self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
657 return ToolbarItemLocation::PrimaryLeft {
658 flex: Some((1., false)),
659 };
660 }
661 }
662 self.tree_view = None;
663 self.subscription = None;
664 ToolbarItemLocation::Hidden
665 }
666}