fix: keep diff explorer below the top bar
This commit is contained in:
@@ -278,6 +278,8 @@ Comparison discovery builds the changed-path/status catalog first. File text, no
|
||||
|
||||
The changed-file explorer is **docked on the right**. Modified files show Before on the left and After on the right. Added files show only After, using the full text workspace; deleted files show only Before. Rename-only files show one empty pane, with the old and new paths in the explorer; renames with text or final-newline changes retain the split view. Before and historical/remote After snapshots are read-only. Selecting **Working checkout** as After binds eligible regular UTF-8 files to normal editor buffers, reusing already-open dirty buffers and their undo history. Editable After supports native edits, completion, undo/redo, and `:w`. Removed files, rename-only files, binary files, symlinks, and submodules remain read-only. Closing the diff keeps its buffers and unsaved edits.
|
||||
|
||||
When a diff occupies the sole workspace pane, its top bar spans the full editor width. The right-docked changed-file explorer starts below that bar rather than covering the header; closing the dock preserves the full-width bar.
|
||||
|
||||
Text panes use normal buffer interaction regardless of writability: motions and counts, search, Visual/Visual Line selections, multiple carets, yank, remapped bindings, and non-mutating actions work in Before and After. Read-only snapshots reject insert/delete/change/paste, repeat edits, undo/redo, mutating plugin effects, and saves, including saves to another path. Their text retains the revision's final newline. Snapshots use separate virtual LSP document identities and do not appear in ordinary buffer lists or dirty-buffer comparisons. Closing the diff disposes unreferenced snapshots and restores the live or original buffer in its workspace pane; snapshots displayed by other panes remain read-only until closed. Explicit native navigation from the comparison pane to an ordinary buffer leaves the comparison at that destination.
|
||||
|
||||
Use the ordinary **file-picker/file-explorer actions and bindings**: Space `o` opens a floating picker, Space `e` opens/focuses the docked explorer, and Ctrl+w closes the focused picker/explorer and reclaims its width. Remapped bindings work too; Ctrl+e is no longer reserved by the diff. The changed-path catalog retains A/M/D/R markers and the normal compact tree, filtering, folder expansion, and configurable `tree`/`selection` navigation. It includes removed and historical paths without scanning the working directory. Enter toggles a folder or opens the selected comparison file and focuses its first visible text pane: After for added files, otherwise Before. While the explorer is hidden, Tab/Shift+Tab cycles between visible text panes only; a single-pane view keeps its focus.
|
||||
|
||||
+87
-34
@@ -412,6 +412,17 @@ impl BufferViewports {
|
||||
Some(document_layout(area, editor))
|
||||
};
|
||||
let full_workspace = chrome.map_or(area, |areas| areas[0]);
|
||||
let diff_header = editor
|
||||
.diff()
|
||||
.filter(|diff| !multiple && diff.phase == Phase::View)
|
||||
.map(|_| {
|
||||
Rect::new(
|
||||
full_workspace.x,
|
||||
full_workspace.y,
|
||||
full_workspace.width,
|
||||
full_workspace.height.min(1),
|
||||
)
|
||||
});
|
||||
let (workspace, dock) = match editor.file_picker() {
|
||||
Some(picker) if picker.presentation == FilePresentation::Docked => dock_layout(
|
||||
full_workspace,
|
||||
@@ -424,6 +435,13 @@ impl BufferViewports {
|
||||
),
|
||||
_ => (full_workspace, None),
|
||||
};
|
||||
let dock = dock.map(|mut panel| {
|
||||
if let Some(header) = diff_header {
|
||||
panel.y = panel.y.saturating_add(header.height);
|
||||
panel.height = panel.height.saturating_sub(header.height);
|
||||
}
|
||||
panel
|
||||
});
|
||||
editor.set_pane_bounds(pane_rect(workspace));
|
||||
self.rects.clear();
|
||||
editor
|
||||
@@ -491,6 +509,11 @@ impl BufferViewports {
|
||||
}
|
||||
self.activate(focused, editor);
|
||||
self.panels.workspace_managed = false;
|
||||
if let Some(header) = diff_header
|
||||
&& let Some(diff) = editor.diff()
|
||||
{
|
||||
draw_diff_header(frame, diff, Some(editor), header);
|
||||
}
|
||||
if let Some(panel) = dock
|
||||
&& let Some(picker) = editor.file_picker()
|
||||
{
|
||||
@@ -1842,6 +1865,46 @@ fn draw_diff_popups(
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_diff_header(frame: &mut Frame, diff: &DiffState, editor: Option<&Editor>, header: Rect) {
|
||||
if header.width == 0 || header.height == 0 {
|
||||
return;
|
||||
}
|
||||
let (before, after) = diff.comparison.as_ref().map_or(("", ""), |comparison| {
|
||||
(comparison.before.as_str(), comparison.after.as_str())
|
||||
});
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!(
|
||||
"Diff · {} → {} · {:?} focus · {}",
|
||||
display_text(before),
|
||||
display_text(after),
|
||||
diff.focus,
|
||||
editor
|
||||
.filter(|editor| editor.diff_interacting())
|
||||
.map_or_else(
|
||||
|| "read-only".into(),
|
||||
|editor| format!(
|
||||
"{} · {}{} · {}:{}",
|
||||
if editor.diff_editing() {
|
||||
"editable"
|
||||
} else {
|
||||
"read-only"
|
||||
},
|
||||
if editor.visual_linewise() {
|
||||
"VISUAL LINE".into()
|
||||
} else {
|
||||
editor.mode().to_string()
|
||||
},
|
||||
if editor.dirty() { " [+]" } else { "" },
|
||||
editor.cursor().row + 1,
|
||||
editor.cursor().column + 1,
|
||||
),
|
||||
)
|
||||
))
|
||||
.style(Style::default().fg(Color::Black).bg(Color::Cyan)),
|
||||
header,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_diff(
|
||||
frame: &mut Frame,
|
||||
diff: &DiffState,
|
||||
@@ -2072,40 +2135,9 @@ fn draw_diff(
|
||||
Constraint::Length(u16::from(area.height >= 3)),
|
||||
])
|
||||
.areas(area);
|
||||
let (before, after) = comparison.map_or(("", ""), |comparison| {
|
||||
(comparison.before.as_str(), comparison.after.as_str())
|
||||
});
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!(
|
||||
"Diff · {} → {} · {:?} focus · {}",
|
||||
display_text(before),
|
||||
display_text(after),
|
||||
diff.focus,
|
||||
editor
|
||||
.filter(|editor| editor.diff_interacting())
|
||||
.map_or_else(
|
||||
|| "read-only".into(),
|
||||
|editor| format!(
|
||||
"{} · {}{} · {}:{}",
|
||||
if editor.diff_editing() {
|
||||
"editable"
|
||||
} else {
|
||||
"read-only"
|
||||
},
|
||||
if editor.visual_linewise() {
|
||||
"VISUAL LINE".into()
|
||||
} else {
|
||||
editor.mode().to_string()
|
||||
},
|
||||
if editor.dirty() { " [+]" } else { "" },
|
||||
editor.cursor().row + 1,
|
||||
editor.cursor().column + 1,
|
||||
),
|
||||
)
|
||||
))
|
||||
.style(Style::default().fg(Color::Black).bg(Color::Cyan)),
|
||||
header,
|
||||
);
|
||||
if !panels.workspace_managed || editor.is_none_or(|editor| editor.pane_layout().len() > 1) {
|
||||
draw_diff_header(frame, diff, editor, header);
|
||||
}
|
||||
if let Some(notice) = notice {
|
||||
frame.render_widget(
|
||||
Paragraph::new(display_text(notice)).style(Style::default().fg(
|
||||
@@ -5059,6 +5091,27 @@ mod tests {
|
||||
assert_eq!(dock.height, 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_diff_dock_stays_below_the_full_width_header() {
|
||||
let mut editor = readonly_diff_editor(deletion_file());
|
||||
let mut views = BufferViewports::default();
|
||||
for (width, height) in [(120, 30), (32, 9)] {
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let dock = views.panels.files.unwrap().0;
|
||||
let before = views.panels.diff.as_ref().unwrap().regions[1].0;
|
||||
assert_eq!(dock.y, before.y);
|
||||
assert_eq!(dock.y, 1);
|
||||
assert_eq!(dock.right(), width);
|
||||
for x in 0..width {
|
||||
assert_eq!(terminal.backend().buffer()[(x, 0)].bg, Color::Cyan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revision_selector_overlays_and_clears_workspace_pane_borders() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user