Implement splitting vertically with syncing

pull/10934/head
lyndon mackay 6 months ago
parent 9123d3fbb8
commit a22de85f95

@ -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<ViewId> = 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) {

@ -1668,6 +1668,41 @@ fn vsplit_new(
Ok(())
}
fn vsplit_extend(
cx: &mut compositor::Context,
_args: &[Cow<str>],
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<str>],
@ -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"],

@ -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();

@ -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<ViewId>,
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<ViewId>,
area: Rect,
synced_views: Vec<SyncedViews>,
}
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<ViewId>, 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<Item = (&View, bool)> {
let focus = self.focus;
self.nodes.iter().filter_map(move |(key, node)| match node {
@ -966,4 +1135,53 @@ mod test {
.collect::<Vec<_>>()
);
}
#[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());
}
}

Loading…
Cancel
Save