From a22de85f954e8c9209cd5e53d73e8088d938c34b Mon Sep 17 00:00:00 2001 From: lyndon mackay Date: Wed, 12 Jun 2024 15:30:42 +1000 Subject: [PATCH] Implement splitting vertically with syncing --- helix-term/src/commands.rs | 95 +++++++++++++ helix-term/src/commands/typed.rs | 42 ++++++ helix-view/src/editor.rs | 16 +++ helix-view/src/tree.rs | 224 ++++++++++++++++++++++++++++++- 4 files changed, 374 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5fc832351..529cfa5e9 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -470,6 +470,7 @@ impl MappableCommand { hsplit, "Horizontal bottom split", hsplit_new, "Horizontal bottom split scratch buffer", vsplit, "Vertical right split", + vsplit_extend, "Vertical right extend split scratch buffer", vsplit_new, "Vertical right split scratch buffer", wclose, "Close window", wonly, "Close windows except current", @@ -648,6 +649,79 @@ fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movem }); drop(annotations); doc.set_selection(view.id, selection); + moved_synced(cx); +} + +pub fn moved_synced(cx: &mut Context) { + let view = view!(cx.editor); + let focused_id = view.id; + + let view_id = view.id; + + let doc = doc!(cx.editor); + let focus_last_line = view.estimate_last_doc_line(doc); + //let focus_last_line = view.last_visual_line(doc); + let focus_inner_height = view.inner_height(); + + let doc = doc_mut!(cx.editor); + + let syncs = cx.editor.tree.get_synced_views(view_id); + + let synced_views: Vec = syncs + .iter() + .find(|x| x.document_id == doc.id()) + .map(|x| x.views.clone()) + .unwrap_or(Vec::new()); + + if synced_views.is_empty() { + return; + } + + let synced_views: Vec<(usize, ViewId)> = synced_views.into_iter().enumerate().collect(); + + let focus_ord_num = synced_views.iter().find(|(_, x)| *x == focused_id); + + let focus_ord_num = match focus_ord_num { + Some(n) => n.0, + None => return, + }; + + for (view, focus) in cx.editor.tree.views_mut() { + let sync_find = synced_views.iter().find(|(_, x)| *x == view.id); + let is_synced = sync_find.is_some(); + + if focus {} + if focus || !is_synced { + continue; + } + + // Just checked if is some above + let (current_order_id, _) = sync_find.unwrap(); + + // if focus is prior view then it is synced above + let offset = if *current_order_id < focus_ord_num { + focus_last_line as isize + } else { + let focus_first_line: usize = + 0.max(focus_last_line as isize - focus_inner_height as isize) as usize; + + 0.max(focus_first_line as isize - view.inner_height() as isize) as isize + }; + + let text_fmt = doc.text_format(view.inner_area(doc).width, None); + let annotations = view.text_annotations(doc, None); + + let doc_text = doc.text().slice(..); + + (view.offset.anchor, view.offset.vertical_offset) = char_idx_at_visual_offset( + doc_text, + 0, + view.offset.vertical_offset as isize + offset as isize, + 0, + &text_fmt, + &annotations, + ); + } } use helix_core::movement::{move_horizontally, move_vertically}; @@ -1718,6 +1792,8 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor ) }); doc.set_selection(view.id, selection); + + moved_synced(cx); return; } @@ -1767,6 +1843,8 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction, sync_cursor sel = sel.replace(idx, prim_sel); drop(annotations); doc.set_selection(view.id, sel); + + moved_synced(cx); } fn page_up(cx: &mut Context) { @@ -1809,6 +1887,7 @@ fn page_cursor_half_up(cx: &mut Context) { let view = view!(cx.editor); let offset = view.inner_height() / 2; scroll(cx, offset, Direction::Backward, true); + moved_synced(cx); } fn page_cursor_half_down(cx: &mut Context) { @@ -5160,6 +5239,17 @@ fn vsplit(cx: &mut Context) { split(cx.editor, Action::VerticalSplit); } +fn vsplit_extend(cx: &mut Context) { + let doc = doc!(cx.editor); + let id = doc.id(); + + cx.editor.switch(id, Action::VerticalSplit); + + if let Some(doc) = cx.editor.document_mut(id) { + doc.mark_as_focused(); + } +} + fn vsplit_new(cx: &mut Context) { cx.editor.new_file(Action::VerticalSplit); } @@ -5219,16 +5309,19 @@ fn insert_register(cx: &mut Context) { fn align_view_top(cx: &mut Context) { let (view, doc) = current!(cx.editor); align_view(doc, view, Align::Top); + moved_synced(cx); } fn align_view_center(cx: &mut Context) { let (view, doc) = current!(cx.editor); align_view(doc, view, Align::Center); + moved_synced(cx); } fn align_view_bottom(cx: &mut Context) { let (view, doc) = current!(cx.editor); align_view(doc, view, Align::Bottom); + moved_synced(cx); } fn align_view_middle(cx: &mut Context) { @@ -5252,10 +5345,12 @@ fn align_view_middle(cx: &mut Context) { fn scroll_up(cx: &mut Context) { scroll(cx, cx.count(), Direction::Backward, false); + moved_synced(cx); } fn scroll_down(cx: &mut Context) { scroll(cx, cx.count(), Direction::Forward, false); + moved_synced(cx); } fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direction) { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 652106bd2..b30849802 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1668,6 +1668,41 @@ fn vsplit_new( Ok(()) } +fn vsplit_extend( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let (view, doc) = current!(cx.editor); + let view_id = view.id; + let id = doc.id(); + + if cx + .editor + .tree + .get_synced_views(view_id) + .iter() + .find(|v| v.document_id == doc.id()) + .is_some() + { + // Only support one split, possible to support more in future + // estimate only changes needed is the scrolling sync logic + cx.editor + .set_error(format!("Only one split per document supported")); + bail!("Only one split per document supported") + } + + cx.editor.switch(id, Action::VerticalSplitSync); + + cx.editor.focus_direction(tree::Direction::Left); + + Ok(()) +} + fn hsplit_new( cx: &mut compositor::Context, _args: &[Cow], @@ -2913,6 +2948,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: vsplit, signature: CommandSignature::all(completers::filename) }, + TypableCommand { + name: "vsplit-extend", + aliases: &["vext"], + doc: "Open the file in a vertical split with scroll syncing", + fun: vsplit_extend, + signature: CommandSignature::all(completers::filename), + }, TypableCommand { name: "vsplit-new", aliases: &["vnew"], diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 635f72619..82c34e4ba 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1117,6 +1117,7 @@ pub enum Action { Replace, HorizontalSplit, VerticalSplit, + VerticalSplitSync, } impl Action { @@ -1591,6 +1592,7 @@ impl Editor { } } } + self.tree.remove_sync_view(view_id); self.replace_document_in_view(view_id, id); @@ -1624,6 +1626,20 @@ impl Editor { doc.ensure_view_init(view_id); doc.mark_as_focused(); } + Action::VerticalSplitSync => { + // copy the current view, unless there is no view yet + let view = self + .tree + .try_get(self.tree.focus) + .filter(|v| id == v.doc) // Different Document + .cloned() + .unwrap_or_else(|| View::new(id, self.config().gutters.clone())); + let view_id = self.tree.split_extend(view); + // initialize selection for view + let doc = doc_mut!(self, &id); + doc.ensure_view_init(view_id); + //doc.mark_as_focused(); + } } self._refresh(); diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index be8bd4e5b..ff47909c2 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -1,4 +1,4 @@ -use crate::{graphics::Rect, View, ViewId}; +use crate::{graphics::Rect, DocumentId, View, ViewId}; use slotmap::HopSlotMap; // the dimensions are recomputed on window resize/tree change. @@ -60,11 +60,18 @@ pub enum Direction { Right, } +#[derive(Clone, Debug)] +pub struct SyncedViews { + pub views: Vec, + pub document_id: crate::DocumentId, // Could change to side by side two similar files as a diff maybe +} + #[derive(Debug)] pub struct Container { layout: Layout, children: Vec, area: Rect, + synced_views: Vec, } impl Container { @@ -73,8 +80,40 @@ impl Container { layout, children: Vec::new(), area: Rect::default(), + synced_views: Vec::new(), } } + + //Add views to sync should be at least two views to sync per document + fn add_sync_views(&mut self, views: Vec, document_id: DocumentId) { + match self + .synced_views + .iter_mut() + .find(|synced| synced.document_id == document_id) + { + Some(sync_view) => sync_view.views.extend(views), + None => { + if views.len() > 1 { + self.synced_views.push(SyncedViews { views, document_id }) + } else { + panic!( + "Trying to sync a less then 1 view this should not happen {}", + views.len() + ) + } + } + } + } + + fn remove_sync_view(&mut self, view: ViewId) { + // remove all view ids from sync list and + // remove remove any synchronisation if + // there is only one view + self.synced_views.retain_mut(|syncs| { + syncs.views.retain_mut(|v_id| *v_id != view); + syncs.views.len() > 2 + }); + } } impl Default for Container { @@ -175,6 +214,90 @@ impl Tree { split.parent = parent; let split = self.nodes.insert(split); + let split_container = match &mut self.nodes[split] { + Node { + content: Content::Container(container), + .. + } => container, + _ => unreachable!(), + }; + split_container.children.push(focus); + split_container.children.push(node); + + self.nodes[focus].parent = split; + self.nodes[node].parent = split; + + let container = match &mut self.nodes[parent] { + Node { + content: Content::Container(container), + .. + } => container, + _ => unreachable!(), + }; + + let pos = container + .children + .iter() + .position(|&child| child == focus) + .unwrap(); + + // replace focus on parent with split + container.children[pos] = split; + self.swap_sync_views(split, parent); + } + + // focus the new node + self.focus = node; + + // recalculate all the sizes + self.recalculate(); + + node + } + + pub fn split_extend(&mut self, view: View) -> ViewId { + let doc_id = view.doc; + let view_id = view.id; + let focus = self.focus; + let parent = self.nodes[focus].parent; + + let node = Node::view(view); + + let node = self.nodes.insert(node); + self.get_mut(node).id = node; + + let node_id = node; + + let container = match &mut self.nodes[parent] { + Node { + content: Content::Container(container), + .. + } => container, + _ => unreachable!(), + }; + if container.layout == Layout::Vertical { + // insert node after the current item if there is children already + let pos = if container.children.is_empty() { + 0 + } else { + let pos = container + .children + .iter() + .position(|&child| child == focus) + .unwrap(); + pos + 1 + }; + + container.children.insert(pos, node); + + container.add_sync_views(vec![node_id, view_id], doc_id); + + self.nodes[node].parent = parent; + } else { + let mut split = Node::container(Layout::Vertical); + split.parent = parent; + let split = self.nodes.insert(split); + let container = match &mut self.nodes[split] { Node { content: Content::Container(container), @@ -203,10 +326,19 @@ impl Tree { // replace focus on parent with split container.children[pos] = split; + + let container = match &mut self.nodes[split] { + Node { + content: Content::Container(c), + .. + } => c, + _ => unreachable!(), + }; + + container.add_sync_views(vec![node_id, view_id], doc_id); } - // focus the new node - self.focus = node; + for (i, n) in self.nodes.iter().enumerate() {} // recalculate all the sizes self.recalculate(); @@ -253,6 +385,8 @@ impl Tree { self.focus = self.prev(); } + self.remove_sync_view(index); + let parent = self.nodes[index].parent; let parent_is_root = parent == self.root; @@ -269,6 +403,41 @@ impl Tree { self.recalculate() } + pub fn remove_sync_view(&mut self, index: ViewId) { + let parent = self.nodes[index].parent; + let container = &mut self.nodes[parent].content; + let container = match container { + Content::Container(c) => c, + Content::View(_) => return, + }; + + container.remove_sync_view(index); + } + + pub fn get_synced_views(&self, view_id: ViewId) -> &[SyncedViews] { + let parent = self.nodes[view_id].parent; + let container = match &self.nodes[parent] { + Node { + content: Content::Container(container), + .. + } => container, + _ => unreachable!(), + }; + container.synced_views.as_slice() + } + + fn swap_sync_views(&mut self, x: ViewId, y: ViewId) { + // These clones are needed to satisfy the borrow checker ideally it would + // simply be a straight call to memswap + let mut sync_views_x = self.container_mut(x).synced_views.clone(); + let mut sync_views_y = self.container_mut(y).synced_views.clone(); + + std::mem::swap(&mut sync_views_x, &mut sync_views_y); + + self.container_mut(x).synced_views = sync_views_x; + self.container_mut(y).synced_views = sync_views_y; + } + pub fn views(&self) -> impl Iterator { let focus = self.focus; self.nodes.iter().filter_map(move |(key, node)| match node { @@ -966,4 +1135,53 @@ mod test { .collect::>() ); } + #[test] + fn split_sync_views() { + let mut tree = Tree::new(Rect { + x: 0, + y: 0, + width: 180, + height: 80, + }); + + let doc_main = DocumentId::default(); + let mut view = View::new(doc_main, GutterConfig::default()); + view.area = Rect::new(0, 0, 180, 80); + tree.insert(view); + + let l0 = tree.focus; + + let view = View::new(doc_main, GutterConfig::default()); + tree.split_extend(view); + let r0 = tree.focus; + + tree.focus = l0; + + // Views in test + // | LO | R1 | + + // Docs in test + // | doc_main | doc_main + + // created a synced split + assert_eq!(tree.get_synced_views(view.id).len(), 1); + + // Synced on the document we created + assert!(tree + .get_synced_views(view.id) + .iter() + .find(|x| x.document_id == doc_main) + .is_some()); + + // the view is on the sync list + assert!(tree + .get_synced_views(view.id) + .iter() + .find(|x| x.views.iter().find(|v| **v == view.id).is_some()) + .is_some()); + + tree.remove_sync_view(view.id); + + assert!(tree.get_synced_views(view.id).is_empty()); + } }