fix: retain selections and float local review editors

This commit is contained in:
Valdemar Lorenzen
2026-10-16 14:16:29 +02:00
parent d67b874b87
commit 4cc0453344
9 changed files with 336 additions and 68 deletions
+229 -39
View File
@@ -678,18 +678,23 @@ impl BufferViewports {
.resolver()
.is_some_and(|resolver| resolver.view == ResolverView::History);
let multiple = !resolver_resolution && editor.pane_layout().len() > 1;
let (workspace, dock) = match editor.file_picker() {
Some(picker) if picker.presentation == FilePresentation::Docked => dock_layout(
full_workspace,
if editor.picker_diff().is_some() {
DockSide::Right
} else {
editor.file_explorer_side()
},
picker.focused,
),
_ => (full_workspace, None),
};
let layout_pane = editor
.review_panel()
.and_then(|pane| editor.pane_origin(pane))
.unwrap_or(focused);
let (workspace, dock) =
editor.with_render_pane(layout_pane, |view| match view.file_picker() {
Some(picker) if picker.presentation == FilePresentation::Docked => dock_layout(
full_workspace,
if view.picker_diff().is_some() {
DockSide::Right
} else {
view.file_explorer_side()
},
picker.focused,
),
_ => (full_workspace, None),
});
editor.set_pane_bounds(pane_rect(workspace));
self.rects.clear();
if resolver_history {
@@ -737,9 +742,6 @@ impl BufferViewports {
.retain(|id, _| view.has_buffer(*id));
let (top, left) = self.offsets.entry(view.buffer_id()).or_default();
let syntax = self.syntax.entry(view.buffer_id()).or_default();
let leaf = view.review_panel_title().map_or(leaf, |title| {
resolver_document_panel(frame, leaf, title, id == focused)
});
draw_document(
frame,
view,
@@ -767,22 +769,25 @@ impl BufferViewports {
self.panels.begin_frame();
self.document.geometry = None;
}
if let Some(panel) = dock
&& let Some(picker) = editor.file_picker()
{
self.panels.picker_scroll.distance = editor.scroll_distance();
draw_file_list(
frame,
picker,
panel,
editor.file_navigation_hint(),
&mut self.panels,
editor.picker_diff().and_then(|diff| {
diff.comparison
.as_ref()
.map(|comparison| (comparison.files.as_slice(), &diff.reviewed_files))
}),
);
if let Some(panel) = dock {
self.activate(layout_pane, editor);
editor.with_render_pane(layout_pane, |view| {
if let Some(picker) = view.file_picker() {
self.panels.picker_scroll.distance = view.scroll_distance();
draw_file_list(
frame,
picker,
panel,
view.file_navigation_hint(),
&mut self.panels,
view.picker_diff().and_then(|diff| {
diff.comparison.as_ref().map(|comparison| {
(comparison.files.as_slice(), &diff.reviewed_files)
})
}),
);
}
});
if let Some(geometry) = &mut self.panels.diff {
geometry.regions[0] = (panel, Focus::Files);
editor.set_diff_pane_bounds(
@@ -827,6 +832,44 @@ impl BufferViewports {
}
}
}
if let Some(panel) = editor.review_panel() {
let source = editor.pane_origin(panel);
let area = self
.rects
.iter()
.find(|(id, _)| Some(*id) == source)
.map_or(full_workspace, |(_, area)| *area);
let area = floating_panel(area);
self.rects.insert(0, (panel, area));
self.activate(panel, editor);
self.panels.workspace_managed = true;
editor.with_render_pane(panel, |view| {
frame.render_widget(Clear, area);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan))
.title(display_text(
view.review_panel_title().unwrap_or("Local review"),
));
let content = block.inner(area);
frame.render_widget(block, area);
self.activate_document(view.buffer_id());
let (top, left) = self.offsets.entry(view.buffer_id()).or_default();
let syntax = self.syntax.entry(view.buffer_id()).or_default();
draw_document(
frame,
view,
content,
full_workspace,
top,
left,
syntax,
&mut self.document,
&mut self.panels,
);
});
self.panels.workspace_managed = false;
}
if let Some(diff) = editor.diff().filter(|diff| diff.phase != Phase::View) {
draw_diff(
frame,
@@ -1097,10 +1140,18 @@ impl BufferViewports {
return true;
}
let focused = session.editor().pane_id();
let Some(target) = self.rects.iter().find_map(|(id, area)| {
let target = self.rects.iter().find_map(|(id, area)| {
area.contains(Position::new(mouse.column, mouse.row))
.then_some(*id)
}) else {
});
let target = if let Some(panel) = session.editor().review_panel() {
if target != Some(panel) {
return true;
}
panel
} else if let Some(target) = target {
target
} else {
return false;
};
if let Some(resolver) = session
@@ -1729,11 +1780,13 @@ fn chrome_items<'a>(
continue;
}
let mut text = match name.as_str() {
"mode" => Cow::Owned(if editor.visual_linewise() {
"VISUAL LINE".into()
} else {
editor.mode().to_string()
}),
"mode" => Cow::Owned(
if editor.mode() == Mode::Visual && editor.visual_linewise() {
"VISUAL LINE".into()
} else {
editor.mode().to_string()
},
),
"path" => Cow::Owned(chrome_title(
"{path}{modified}",
editor.tab_index() + 1,
@@ -13962,7 +14015,7 @@ end
editor.apply_diff_result(
id,
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
root: std::path::PathBuf::new(),
root: editor.project_root().to_path_buf(),
editable_paths: Default::default(),
before: "baseline".into(),
after: "historic".into(),
@@ -13983,6 +14036,143 @@ end
terminal.get_cursor_position().unwrap()
}
#[test]
fn command_input_keeps_characterwise_and_linewise_selection_highlights() {
for context in ["document", "split", "unified"] {
for selector in ['v', 'V'] {
let mut editor = if context == "document" {
let mut editor = Editor::open(None, vec![]).unwrap();
editor
.apply(vec![crate::editor::Effect::Replace(
"alpha beta\nsecond line".into(),
)])
.unwrap();
editor
} else {
let mut editor = readonly_diff_editor(syntax_diff_file(
"file.txt",
"alpha beta\nold line",
"alpha beta\nnew line",
));
if context == "unified" {
editor.execute("diff.view.toggle");
}
editor.handle(Input::Char('\t'));
editor
};
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(140, 24)).unwrap();
let mut views = BufferViewports::default();
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let source = terminal.get_cursor_position().unwrap();
editor.handle(Input::Char(selector));
editor.handle(Input::Right);
let ranges = editor.selections();
editor.handle(Input::Char(':'));
for ch in "diff.comment".chars() {
editor.handle(Input::Char(ch));
}
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(editor.mode(), Mode::Command);
assert_eq!(editor.selections(), ranges);
let buffer = terminal.backend().buffer();
assert_eq!(
buffer[(source.x, source.y)].bg,
Color::Blue,
"{context} {selector}"
);
assert_eq!(
buffer[(source.x + if selector == 'V' { 9 } else { 1 }, source.y)].bg,
Color::Blue,
"{context} {selector}"
);
editor.handle(Input::Escape);
assert_eq!(editor.selections(), ranges);
}
}
}
#[test]
fn review_editor_floats_over_the_diff_without_changing_workspace_layout() {
for unified in [false, true] {
let mut editor = readonly_diff_editor(syntax_diff_file(
"file.txt",
"old first\nold last",
"new first\nnew last",
));
if unified {
editor.execute("diff.view.toggle");
}
editor.handle(Input::Char('\t'));
let owner = editor.pane_id();
let source_text = editor.context().text;
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 30)).unwrap();
let mut views = BufferViewports::default();
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let source_area = views
.rects
.iter()
.find(|(pane, _)| *pane == owner)
.unwrap()
.1;
editor.execute("diff.comment");
let panel = editor.review_panel().expect("focused floating review");
assert_eq!(editor.pane_layout().len(), 1);
assert!(!editor.pane_layout().contains(panel));
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(views.rects[0], (panel, floating_panel(source_area)));
assert_eq!(
views
.rects
.iter()
.find(|(pane, _)| *pane == owner)
.unwrap()
.1,
source_area
);
let area = views.rects[0].1;
let cursor = terminal.get_cursor_position().unwrap();
assert!(area.contains(cursor));
for input in [
Input::Char('i'),
Input::Paste("Retained draft".into()),
Input::Escape,
] {
editor.handle(input);
}
assert_eq!(editor.context().text, "Retained draft");
assert_eq!(editor.review_panel(), Some(panel));
editor.handle(Input::Escape);
assert!(editor.review_panel().is_none());
assert_eq!(editor.pane_id(), owner);
assert_eq!(editor.context().text, source_text);
editor.execute("diff.review.edit 1");
assert_eq!(
editor.context().text,
"Retained draft",
"{}",
editor.message()
);
assert_eq!(editor.review_panel(), Some(panel));
editor.execute("tab.new");
editor.execute("tab.previous");
editor.execute("diff.review.edit 1");
assert_eq!(editor.review_panel(), Some(panel));
assert_eq!(editor.context().text, "Retained draft");
editor.execute("pane.close");
assert!(editor.review_panel().is_none());
assert_eq!(editor.pane_layout().len(), 1);
}
}
#[test]
fn scope_projection_keeps_unified_wrapped_caret_and_selection_source_only() {
let before = format!(