feat: add configurable three-pane conflict views

This commit is contained in:
Valdemar Lorenzen
2026-10-16 16:19:54 +02:00
parent a6310594c5
commit de6a57e67d
11 changed files with 176 additions and 20 deletions
+97 -5
View File
@@ -45,8 +45,8 @@ use crate::{
RowKind, ViewMode,
},
editor::{
Completion, Cursor, Editor, Hover, Input, Mode, Palette, ResolverStatus, ResolverView,
ResolverWorkspace, Selection, StartupDiagnostic, WhitespaceScope,
Completion, Cursor, Editor, Hover, Input, Mode, Palette, ResolverLayout, ResolverStatus,
ResolverView, ResolverWorkspace, Selection, StartupDiagnostic, WhitespaceScope,
},
files::{DockSide, FilePicker, FilePresentation, FileView},
panes::{PaneRect, SplitAxis, split_rect},
@@ -698,7 +698,12 @@ impl BufferViewports {
editor.set_pane_bounds(pane_rect(workspace));
self.rects.clear();
if resolver_history {
let layout = resolver_history_layout(workspace, editor.resolver().unwrap(), focused);
let layout = resolver_history_layout(
workspace,
editor.resolver().unwrap(),
focused,
editor.resolve_history_layout,
);
draw_resolver_heading(frame, editor.resolver().unwrap(), layout.heading);
self.draw_resolver_history(frame, editor, &layout);
} else if resolver_resolution {
@@ -963,7 +968,7 @@ impl BufferViewports {
.iter()
.flatten()
.map(|(_, _, area)| usize::from(area.height))
.max()
.min()
.unwrap_or(0);
history.top.continuation = history
.top
@@ -2097,6 +2102,7 @@ fn resolver_history_layout(
area: Rect,
resolver: &ResolverWorkspace,
focused: usize,
mode: ResolverLayout,
) -> ResolverHistoryLayout {
let [heading, documents] = Layout::vertical([
Constraint::Length(u16::from(area.height >= 4)),
@@ -2126,7 +2132,8 @@ fn resolver_history_layout(
sources.push(index);
}
}
if documents.width < 60 {
let base_below = mode == ResolverLayout::BaseBelow && sources.len() == 3;
if documents.width < 60 || (base_below && documents.height < 4) {
let selected = sources
.iter()
.copied()
@@ -2135,6 +2142,12 @@ fn resolver_history_layout(
if let Some(selected) = selected {
layout.history.push((selected, documents));
}
} else if base_below {
let [inputs, base] = Layout::vertical([Constraint::Fill(1); 2]).areas(documents);
let [left, right] = Layout::horizontal([Constraint::Fill(1); 2]).areas(inputs);
layout
.history
.extend([(sources[0], left), (sources[2], right), (sources[1], base)]);
} else {
let columns = Layout::horizontal(vec![Constraint::Fill(1); sources.len()]).split(documents);
layout
@@ -8183,6 +8196,85 @@ mod tests {
assert_eq!(session.editor().cursor(), draft_cursor);
assert_eq!(views.panels.diff.as_ref().unwrap().viewport, draft_viewport);
assert_eq!(restored_caret, searched_caret);
{
let config = directory.file(
"base-below.rb",
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\n config.resolve_history_layout = 'base_below'\nend\n",
);
let mut bottom = EditorSession::open(
Some(directory.0.join("first.txt")),
vec!["text".into()],
Some(&config),
)
.unwrap();
bottom.execute("resolve");
ready(&mut bottom);
let draft = bottom.editor().context().text;
bottom.execute("resolve.history");
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 30)).unwrap();
let mut views = BufferViewports::default();
render(&mut terminal, &mut views, &mut bottom);
let resolver = bottom.editor().resolver().unwrap();
let left = resolver.history_panes[resolver.history_sources[0]];
let right = resolver.history_panes[resolver.history_sources[1]];
let base = resolver.history_panes[resolver.selected_base.unwrap()];
assert_eq!(views.rects.len(), 3);
assert_eq!(
views
.rects
.iter()
.map(|(pane, _)| *pane)
.collect::<Vec<_>>(),
[left, right, base]
);
let [left_area, right_area, base_area] =
std::array::from_fn(|index| views.rects[index].1);
assert_eq!(left_area.right(), right_area.x);
assert_eq!(left_area.y, right_area.y);
assert_eq!(base_area.x, left_area.x);
assert_eq!(base_area.right(), right_area.right());
assert!(base_area.y >= left_area.bottom());
for (area, value) in [
(left_area, "value = left;"),
(right_area, "value = right;"),
(base_area, "value = base;"),
] {
assert!(line_at(&terminal, area, area.y).contains(value));
}
bottom.handle(Input::Char('\t'));
assert_eq!(bottom.editor().pane_id(), right);
bottom.handle(Input::Char('\t'));
assert_eq!(bottom.editor().pane_id(), base);
bottom.handle(Input::BackTab);
assert_eq!(bottom.editor().pane_id(), right);
bottom.execute("pane.focus_down");
let caret = render(&mut terminal, &mut views, &mut bottom);
assert_eq!(bottom.editor().pane_id(), base);
assert!(base_area.contains(caret));
assert!(views.scroll(
&mut bottom,
document_wheel(MouseEventKind::ScrollDown, base_area.x, base_area.y)
));
render(&mut terminal, &mut views, &mut bottom);
for area in [left_area, right_area, base_area] {
assert!(line_at(&terminal, area, area.y).contains("context 02"));
}
let readonly = bottom.editor().context().text;
bottom.handle(Input::Char('i'));
bottom.handle(Input::Paste("must not edit base".into()));
bottom.handle(Input::Escape);
assert_eq!(bottom.editor().context().text, readonly);
terminal.backend_mut().resize(44, 20);
render(&mut terminal, &mut views, &mut bottom);
assert_eq!(views.rects.len(), 1);
assert_eq!(views.rects[0].0, base);
terminal.backend_mut().resize(120, 6);
render(&mut terminal, &mut views, &mut bottom);
assert_eq!(views.rects.len(), 1);
assert_eq!(views.rects[0].0, base);
bottom.execute("resolve.draft");
assert_eq!(bottom.editor().context().text, draft);
}
for (key, action) in [
("ctrl+e", "core.file_explorer"),