feat: close focused docked file lists with pane actions

This commit is contained in:
Valdemar Lorenzen
2026-10-12 15:42:58 +02:00
parent d64704d4eb
commit 1fd5f98256
4 changed files with 1094 additions and 17 deletions
+294
View File
@@ -11258,6 +11258,300 @@ end
assert!(screen_text(&terminal).contains(editor.selection_navigation_hint()));
}
const PANE_CLOSE_CONFIG: &str = r#"
Vamp.configure do |config|
config.file_explorer_view = "list"
config.file_explorer_side = "right"
config.file_picker_view = "tree"
config.bind "ctrl+o", config.core.file_picker
config.bind "ctrl+x", config.core.pane.close
config.bind "ctrl+y", config.core.pane.force_close
end
"#;
#[test]
fn bound_pane_close_reclaims_focused_dock_without_losing_dirty_source_or_diff() {
let directory = Directory::new();
let path = directory.file("source.txt", "SOURCE\nsecond");
let config = directory.file("config.rb", PANE_CLOSE_CONFIG);
for comparison in [false, true] {
for key in ['x', 'y'] {
let mut session =
EditorSession::open(Some(path.clone()), vec![], Some(&config)).unwrap();
session.editor_mut().set_project_root(directory.0.clone());
let source = session.editor().buffer_id();
session.handle(Input::Char('i'));
session.handle(Input::Paste("DIRTY ".into()));
session.handle(Input::Escape);
let text = session.editor().lines().to_vec();
if comparison {
install_readonly_diff(
session.editor_mut(),
syntax_diff_file("history.txt", "OLD", "NEW"),
);
} else {
session.handle(Input::Char(' '));
session.handle(Input::Char('e'));
}
let pane = session.editor().pane_id();
let tab = session.editor().tab_id();
let buffers = session.editor().buffer_count();
let comparison_id = session.editor().diff().and_then(|diff| diff.comparison_id);
let picker = session.editor().file_picker().unwrap();
assert!(picker.focused);
assert_eq!(picker.presentation, FilePresentation::Docked);
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(120, 20)).unwrap();
let mut views = BufferViewports::default();
terminal
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
let dock = views.panels.files.unwrap().0;
let workspace = views.rects[0].1;
let regions = views.panels.diff.as_ref().map(|geometry| geometry.regions);
assert_eq!(workspace.width + dock.width, 120);
assert!(dock.contains(terminal.get_cursor_position().unwrap()));
session.handle(
translate(KeyEvent::new(KeyCode::Char(key), KeyModifiers::CONTROL)).unwrap(),
);
terminal
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
assert!(!session.editor().should_quit(), "{comparison} Ctrl+{key}");
assert_eq!(session.editor().pane_id(), pane);
assert_eq!(session.editor().tab_id(), tab);
assert_eq!(session.editor().pane_layout().len(), 1);
assert_eq!(session.editor().tab_count(), 1);
assert_eq!(session.editor().buffer_count(), buffers);
assert!(session.editor().file_picker().is_none());
assert!(views.panels.files.is_none());
assert_eq!(views.rects[0].1.width, 120);
assert!(!screen_text(&terminal).contains("Explorer"));
assert!(
session
.editor()
.buffers()
.find(|buffer| buffer.id == source)
.unwrap()
.dirty
);
assert_eq!(
session
.editor()
.documents()
.find(|document| document.id == source)
.unwrap()
.lines,
text
);
let (caret_area, caret) = views.panels.caret.unwrap();
assert!(caret_area.contains(caret));
assert_eq!(terminal.get_cursor_position().unwrap(), caret);
if comparison {
let diff = session.editor().diff().unwrap();
assert_eq!(diff.comparison_id, comparison_id);
assert!(!diff.files_visible);
assert!(!diff.files.as_ref().unwrap().focused);
let expanded = views.panels.diff.as_ref().unwrap().regions;
assert_eq!(expanded[0].0, Rect::default());
for index in [1, 2] {
assert!(expanded[index].0.width > regions.unwrap()[index].0.width);
assert_eq!(
terminal.backend().buffer()[(expanded[index].0.x, expanded[index].0.y)]
.symbol(),
"┌"
);
}
let screen = screen_text(&terminal);
assert!(screen.contains("Before") && screen.contains("After"));
assert!(screen.contains("OLD") && screen.contains("NEW"));
session.execute("diff.close");
}
assert_eq!(session.editor().buffer_id(), source);
assert_eq!(session.editor().lines(), text);
session.handle(Input::Char('u'));
assert_eq!(session.editor().lines(), ["SOURCE", "second"]);
assert!(!session.editor().dirty());
}
}
}
#[test]
fn bound_pane_close_with_unfocused_dock_removes_editor_leaf_not_the_list() {
let directory = Directory::new();
let path = directory.file("source.txt", "SOURCE");
let config = directory.file("config.rb", PANE_CLOSE_CONFIG);
for comparison in [false, true] {
let mut session =
EditorSession::open(Some(path.clone()), vec![], Some(&config)).unwrap();
session.editor_mut().set_project_root(directory.0.clone());
let root = session.editor().pane_id();
let source = session.editor().buffer_id();
session.handle(Input::Char('i'));
session.handle(Input::Paste("DIRTY ".into()));
session.handle(Input::Escape);
session.execute("pane.split_vertical");
let closing = session.editor().pane_id();
if comparison {
install_readonly_diff(
session.editor_mut(),
syntax_diff_file("history.txt", "OLD", "NEW"),
);
} else {
session.handle(Input::Char(' '));
session.handle(Input::Char('e'));
}
session.handle(Input::Char('\t'));
assert!(!session.editor().file_picker().unwrap().focused);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 20)).unwrap();
let mut views = BufferViewports::default();
terminal
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
assert_eq!(views.rects.len(), 2);
let dock = views.panels.files.unwrap().0;
session.handle(Input::Ctrl('x'));
terminal
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
assert!(!session.editor().should_quit());
assert_eq!(session.editor().pane_id(), root);
assert!(!session.editor().pane_layout().contains(closing));
assert_eq!(views.rects.len(), 1);
assert_eq!(session.editor().buffer_id(), source);
assert_eq!(session.editor().lines(), ["DIRTY SOURCE"]);
assert!(session.editor().dirty());
assert!(session.editor().diff().is_none());
assert!(!screen_text(&terminal).contains("Before"));
if comparison {
assert!(views.panels.files.is_none());
assert_eq!(views.rects[0].1.width, 120);
} else {
assert!(!session.editor().file_picker().unwrap().focused);
assert_eq!(views.panels.files.unwrap().0, dock);
assert_eq!(views.rects[0].1.width + dock.width, 120);
assert!(screen_text(&terminal).contains("Explorer"));
}
assert!(
views.rects[0]
.1
.contains(terminal.get_cursor_position().unwrap())
);
}
}
#[test]
fn bound_pane_close_under_floating_picker_obeys_dirty_guard_force_and_escape() {
let directory = Directory::new();
let source = directory.file("source.txt", "SURVIVING DOCUMENT");
let owner = directory.file("owner.txt", "POPUP OWNER");
let config = directory.file("config.rb", PANE_CLOSE_CONFIG);
for comparison in [false, true] {
for dirty in [false, true] {
let mut session =
EditorSession::open(Some(source.clone()), vec![], Some(&config)).unwrap();
session.editor_mut().set_project_root(directory.0.clone());
let root = session.editor().pane_id();
let source_buffer = session.editor().buffer_id();
session.execute("tab.new");
session.execute(&format!("edit {}", owner.display()));
let owner_buffer = session.editor().buffer_id();
let owner_pane = session.editor().pane_id();
let owner_tab = session.editor().tab_id();
if dirty {
session.handle(Input::Char('i'));
session.handle(Input::Paste("DIRTY ".into()));
session.handle(Input::Escape);
}
let text = session.editor().lines().to_vec();
if comparison {
install_readonly_diff(
session.editor_mut(),
syntax_diff_file("history.txt", "OLD", "NEW"),
);
}
let comparison_id = session.editor().diff().and_then(|diff| diff.comparison_id);
session.handle(Input::Ctrl('o'));
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(120, 20)).unwrap();
let mut views = BufferViewports::default();
terminal
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
let popup = views.panels.files.unwrap().0;
assert_eq!(
session.editor().file_picker().unwrap().presentation,
FilePresentation::Floating
);
assert!(popup.contains(terminal.get_cursor_position().unwrap()));
session.handle(Input::Ctrl('x'));
if dirty {
terminal
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
assert_eq!(session.editor().tab_count(), 2);
assert_eq!(session.editor().tab_id(), owner_tab);
assert_eq!(session.editor().pane_id(), owner_pane);
assert!(!session.editor().should_quit());
assert!(session.editor().message().contains("Unsaved changes"));
assert!(session.editor().file_picker().unwrap().focused);
assert_eq!(views.panels.files.unwrap().0, popup);
assert!(popup.contains(terminal.get_cursor_position().unwrap()));
assert_eq!(
session
.editor()
.documents()
.find(|document| document.id == owner_buffer)
.unwrap()
.lines,
text
);
session.handle(Input::Escape);
assert!(session.editor().file_picker().unwrap().focused);
assert!(!session.editor().file_picker().unwrap().searching);
session.handle(Input::Escape);
terminal
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
assert!(session.editor().file_picker().is_none());
assert!(views.panels.files.is_none());
assert_eq!(session.editor().pane_id(), owner_pane);
assert_eq!(session.editor().tab_count(), 2);
assert_eq!(
session.editor().diff().and_then(|diff| diff.comparison_id),
comparison_id
);
if comparison {
assert!(screen_text(&terminal).contains("Before"));
} else {
assert!(screen_text(&terminal).contains("DIRTY POPUP OWNER"));
}
session.handle(Input::Ctrl('o'));
session.handle(Input::Ctrl('y'));
}
terminal
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
assert!(!session.editor().should_quit());
assert_eq!(session.editor().tab_count(), 1);
assert_eq!(session.editor().pane_id(), root);
assert_eq!(session.editor().buffer_id(), source_buffer);
assert!(!session.editor().has_buffer(owner_buffer));
assert_eq!(session.editor().lines(), ["SURVIVING DOCUMENT"]);
assert!(session.editor().diff().is_none());
assert!(session.editor().file_picker().is_none());
assert!(views.panels.files.is_none());
assert_eq!(views.rects.len(), 1);
assert_eq!(views.rects[0].1.width, 120);
assert!(screen_text(&terminal).contains("SURVIVING DOCUMENT"));
let (caret_area, caret) = views.panels.caret.unwrap();
assert!(caret_area.contains(caret));
assert_eq!(terminal.get_cursor_position().unwrap(), caret);
}
}
}
#[test]
fn all_dock_sides_clip_editor_gutter_selection_carets_and_cursor() {
let mut editor = Editor::open(None, vec![]).unwrap();