Sync selection between syntax tree view and editor

This commit is contained in:
Max Brunsfeld 2023-06-09 16:11:29 -07:00
parent 086cfe57c5
commit e969e3b028
3 changed files with 169 additions and 105 deletions

View file

@ -7102,7 +7102,7 @@ impl Editor {
let mut new_selections_by_buffer = HashMap::default(); let mut new_selections_by_buffer = HashMap::default();
for selection in editor.selections.all::<usize>(cx) { for selection in editor.selections.all::<usize>(cx) {
for (buffer, mut range) in for (buffer, mut range, _) in
buffer.range_to_buffer_ranges(selection.start..selection.end, cx) buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
{ {
if selection.reversed { if selection.reversed {

View file

@ -1140,7 +1140,7 @@ impl MultiBuffer {
&self, &self,
range: Range<T>, range: Range<T>,
cx: &AppContext, cx: &AppContext,
) -> Vec<(ModelHandle<Buffer>, Range<usize>)> { ) -> Vec<(ModelHandle<Buffer>, Range<usize>, ExcerptId)> {
let snapshot = self.read(cx); let snapshot = self.read(cx);
let start = range.start.to_offset(&snapshot); let start = range.start.to_offset(&snapshot);
let end = range.end.to_offset(&snapshot); let end = range.end.to_offset(&snapshot);
@ -1165,7 +1165,7 @@ impl MultiBuffer {
let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start()); let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start());
let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start()); let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start());
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
result.push((buffer, start..end)); result.push((buffer, start..end, excerpt.id));
cursor.next(&()); cursor.next(&());
} }
@ -5196,7 +5196,7 @@ mod tests {
.range_to_buffer_ranges(start_ix..end_ix, cx); .range_to_buffer_ranges(start_ix..end_ix, cx);
let excerpted_buffers_text = excerpted_buffer_ranges let excerpted_buffers_text = excerpted_buffer_ranges
.iter() .iter()
.map(|(buffer, buffer_range)| { .map(|(buffer, buffer_range, _)| {
buffer buffer
.read(cx) .read(cx)
.text_for_range(buffer_range.clone()) .text_for_range(buffer_range.clone())

View file

@ -1,7 +1,7 @@
use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
use gpui::{ use gpui::{
actions, actions,
elements::{Empty, Label, MouseEventHandler, UniformList, UniformListState}, elements::{Empty, Label, MouseEventHandler, ScrollTarget, UniformList, UniformListState},
fonts::TextStyle, fonts::TextStyle,
platform::MouseButton, platform::MouseButton,
AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle,
@ -26,14 +26,24 @@ pub fn init(cx: &mut AppContext) {
} }
pub struct SyntaxTreeView { pub struct SyntaxTreeView {
editor: Option<(ViewHandle<Editor>, gpui::Subscription)>, editor: Option<EditorState>,
buffer: Option<(ModelHandle<Buffer>, usize, ExcerptId)>, mouse_y: Option<f32>,
layer: Option<OwnedSyntaxLayerInfo>,
hover_y: Option<f32>,
line_height: Option<f32>, line_height: Option<f32>,
list_state: UniformListState, list_state: UniformListState,
active_descendant_ix: Option<usize>, selected_descendant_ix: Option<usize>,
highlighted_active_descendant: bool, hovered_descendant_ix: Option<usize>,
}
struct EditorState {
editor: ViewHandle<Editor>,
active_buffer: Option<BufferState>,
_subscription: gpui::Subscription,
}
struct BufferState {
buffer: ModelHandle<Buffer>,
excerpt_id: ExcerptId,
active_layer: Option<OwnedSyntaxLayerInfo>,
} }
impl SyntaxTreeView { impl SyntaxTreeView {
@ -41,12 +51,10 @@ impl SyntaxTreeView {
let mut this = Self { let mut this = Self {
list_state: UniformListState::default(), list_state: UniformListState::default(),
editor: None, editor: None,
buffer: None, mouse_y: None,
layer: None,
hover_y: None,
line_height: None, line_height: None,
active_descendant_ix: None, hovered_descendant_ix: None,
highlighted_active_descendant: false, selected_descendant_ix: None,
}; };
this.workspace_updated(workspace.active_item(cx), cx); this.workspace_updated(workspace.active_item(cx), cx);
@ -76,112 +84,157 @@ impl SyntaxTreeView {
} }
fn set_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) { fn set_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
if let Some((current_editor, _)) = &self.editor { if let Some(state) = &self.editor {
if current_editor == &editor { if state.editor == editor {
return; return;
} }
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.clear_background_highlights::<Self>(cx); editor.clear_background_highlights::<Self>(cx)
}); });
} }
let subscription = cx.subscribe(&editor, |this, editor, event, cx| { let subscription = cx.subscribe(&editor, |this, _, event, cx| {
let selection_changed = match event { let reset_layer = match event {
editor::Event::Reparsed => false, editor::Event::Reparsed => true,
editor::Event::SelectionsChanged { .. } => true, editor::Event::SelectionsChanged { .. } => false,
_ => return, _ => return,
}; };
this.editor_updated(&editor, selection_changed, cx); this.editor_updated(reset_layer, cx);
}); });
self.editor_updated(&editor, true, cx); self.editor = Some(EditorState {
self.editor = Some((editor, subscription)); editor,
_subscription: subscription,
active_buffer: None,
});
self.editor_updated(true, cx);
} }
fn editor_updated( fn editor_updated(&mut self, reset_layer: bool, cx: &mut ViewContext<Self>) -> Option<()> {
&mut self, // Find which excerpt the cursor is in, and the position within that excerpted buffer.
editor: &ViewHandle<Editor>, let editor_state = self.editor.as_mut()?;
selection_changed: bool, let editor = &editor_state.editor.read(cx);
cx: &mut ViewContext<Self>, let selection_range = editor.selections.last::<usize>(cx).range();
) { let multibuffer = editor.buffer().read(cx);
let editor = editor.read(cx); let (buffer, range, excerpt_id) = multibuffer
if selection_changed { .range_to_buffer_ranges(selection_range, cx)
let cursor = editor.selections.last::<usize>(cx).end; .pop()?;
self.buffer = editor.buffer().read(cx).point_to_buffer_offset(cursor, cx);
self.layer = self.buffer.as_ref().and_then(|(buffer, offset, _)| { // If the cursor has moved into a different excerpt, retrieve a new syntax layer
buffer // from that buffer.
.read(cx) let buffer_state = editor_state
.snapshot() .active_buffer
.syntax_layer_at(*offset) .get_or_insert_with(|| BufferState {
.map(|l| l.to_owned()) buffer: buffer.clone(),
excerpt_id,
active_layer: None,
}); });
if reset_layer
|| buffer_state.buffer != buffer
|| buffer_state.excerpt_id != buffer_state.excerpt_id
{
buffer_state.buffer = buffer.clone();
buffer_state.excerpt_id = excerpt_id;
buffer_state.active_layer = None;
} }
// Within the active layer, find the syntax node under the cursor,
// and scroll to it.
let layer = match &mut buffer_state.active_layer {
Some(layer) => layer,
None => {
let layer = buffer.read(cx).snapshot().syntax_layer_at(0)?.to_owned();
buffer_state.active_layer.insert(layer)
}
};
let mut cursor = layer.node().walk();
while cursor.goto_first_child_for_byte(range.start).is_some() {
if !range.is_empty() && cursor.node().end_byte() == range.start {
cursor.goto_next_sibling();
}
}
// Ascend to the smallest ancestor that contains the range.
loop {
let node_range = cursor.node().byte_range();
if node_range.start <= range.start && node_range.end >= range.end {
break;
}
if !cursor.goto_parent() {
break;
}
}
let descendant_ix = cursor.descendant_index();
self.selected_descendant_ix = Some(descendant_ix);
self.list_state.scroll_to(ScrollTarget::Show(descendant_ix));
cx.notify(); cx.notify();
Some(())
}
fn handle_click(&mut self, y: f32, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
let line_height = self.line_height?;
let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
selections.select_ranges(vec![range]);
});
});
Some(())
} }
fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) { fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) {
if let Some((y, line_height)) = self.hover_y.zip(self.line_height) { if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) {
let ix = ((self.list_state.scroll_top() + y) / line_height) as usize; let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
if self.active_descendant_ix != Some(ix) { if self.hovered_descendant_ix != Some(ix) {
self.active_descendant_ix = Some(ix); self.hovered_descendant_ix = Some(ix);
self.highlighted_active_descendant = false; self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
editor.clear_background_highlights::<Self>(cx);
editor.highlight_background::<Self>(
vec![range],
|theme| theme.editor.document_highlight_write_background,
cx,
);
});
cx.notify(); cx.notify();
} }
} }
} }
fn handle_click(&mut self, y: f32, cx: &mut ViewContext<SyntaxTreeView>) { fn update_editor_with_range_for_descendant_ix(
if let Some(line_height) = self.line_height {
let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
if let Some(layer) = &self.layer {
let mut cursor = layer.node().walk();
cursor.goto_descendant(ix);
let node = cursor.node();
self.update_editor_with_node_range(node, cx, |editor, range, cx| {
editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
selections.select_ranges(vec![range]);
});
});
}
}
}
fn update_editor_with_node_range(
&self, &self,
node: tree_sitter::Node, descendant_ix: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>), mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>),
) { ) -> Option<()> {
let range = node.byte_range(); let editor_state = self.editor.as_ref()?;
if let Some((editor, _)) = &self.editor { let buffer_state = editor_state.active_buffer.as_ref()?;
if let Some((buffer, _, excerpt_id)) = &self.buffer { let layer = buffer_state.active_layer.as_ref()?;
let buffer = &buffer.read(cx);
let multibuffer = editor.read(cx).buffer();
let multibuffer = multibuffer.read(cx).snapshot(cx);
let start =
multibuffer.anchor_in_excerpt(*excerpt_id, buffer.anchor_before(range.start));
let end =
multibuffer.anchor_in_excerpt(*excerpt_id, buffer.anchor_after(range.end));
editor.update(cx, |editor, cx| {
f(editor, start..end, cx);
});
}
}
}
fn node_is_active(&mut self, node: tree_sitter::Node, cx: &mut ViewContext<Self>) { // Find the node.
if self.highlighted_active_descendant { let mut cursor = layer.node().walk();
return; cursor.goto_descendant(descendant_ix);
} let node = cursor.node();
self.highlighted_active_descendant = true; let range = node.byte_range();
self.update_editor_with_node_range(node, cx, |editor, range, cx| {
editor.clear_background_highlights::<Self>(cx); // Build a text anchor range.
editor.highlight_background::<Self>( let buffer = buffer_state.buffer.read(cx);
vec![range], let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
|theme| theme.editor.document_highlight_write_background,
cx, // Build a multibuffer anchor range.
); let multibuffer = editor_state.editor.read(cx).buffer();
let multibuffer = multibuffer.read(cx).snapshot(cx);
let excerpt_id = buffer_state.excerpt_id;
let range = multibuffer.anchor_in_excerpt(excerpt_id, range.start)
..multibuffer.anchor_in_excerpt(excerpt_id, range.end);
// Update the editor with the anchor range.
editor_state.editor.update(cx, |editor, cx| {
f(editor, range, cx);
}); });
Some(())
} }
} }
@ -215,18 +268,27 @@ impl View for SyntaxTreeView {
font_properties: Default::default(), font_properties: Default::default(),
underline: Default::default(), underline: Default::default(),
}; };
self.line_height = Some(cx.font_cache().line_height(font_size));
self.hover_state_changed(cx); let line_height = Some(cx.font_cache().line_height(font_size));
if line_height != self.line_height {
self.line_height = line_height;
self.hover_state_changed(cx);
}
if let Some(layer) = &self.layer { if let Some(layer) = self
.editor
.as_ref()
.and_then(|editor| editor.active_buffer.as_ref())
.and_then(|buffer| buffer.active_layer.as_ref())
{
let layer = layer.clone(); let layer = layer.clone();
return MouseEventHandler::<Self, Self>::new(0, cx, move |_, cx| { return MouseEventHandler::<Self, Self>::new(0, cx, move |state, cx| {
let list_hovered = state.hovered();
UniformList::new( UniformList::new(
self.list_state.clone(), self.list_state.clone(),
layer.node().descendant_count(), layer.node().descendant_count(),
cx, cx,
move |this, range, items, cx| { move |this, range, items, _| {
let mut cursor = layer.node().walk(); let mut cursor = layer.node().walk();
let mut descendant_ix = range.start as usize; let mut descendant_ix = range.start as usize;
cursor.goto_descendant(descendant_ix); cursor.goto_descendant(descendant_ix);
@ -243,19 +305,19 @@ impl View for SyntaxTreeView {
} }
} else { } else {
let node = cursor.node(); let node = cursor.node();
let is_hovered = Some(descendant_ix) == this.active_descendant_ix; let hovered = Some(descendant_ix) == this.hovered_descendant_ix;
if is_hovered { let selected = Some(descendant_ix) == this.selected_descendant_ix;
this.node_is_active(node, cx);
}
items.push( items.push(
Label::new(node.kind(), style.clone()) Label::new(node.kind(), style.clone())
.contained() .contained()
.with_background_color(if is_hovered { .with_background_color(if selected {
editor_theme.selection.selection
} else if hovered && list_hovered {
editor_theme.active_line_background editor_theme.active_line_background
} else { } else {
Default::default() Default::default()
}) })
.with_padding_left(depth as f32 * 10.0) .with_padding_left(depth as f32 * 18.0)
.into_any(), .into_any(),
); );
descendant_ix += 1; descendant_ix += 1;
@ -271,13 +333,15 @@ impl View for SyntaxTreeView {
}) })
.on_move(move |event, this, cx| { .on_move(move |event, this, cx| {
let y = event.position.y() - event.region.origin_y(); let y = event.position.y() - event.region.origin_y();
this.hover_y = Some(y); this.mouse_y = Some(y);
this.hover_state_changed(cx); this.hover_state_changed(cx);
}) })
.on_click(MouseButton::Left, move |event, this, cx| { .on_click(MouseButton::Left, move |event, this, cx| {
let y = event.position.y() - event.region.origin_y(); let y = event.position.y() - event.region.origin_y();
this.handle_click(y, cx); this.handle_click(y, cx);
}) })
.contained()
.with_background_color(editor_theme.background)
.into_any(); .into_any();
} }