feat: add editor tabs
This commit is contained in:
+321
-9
@@ -403,9 +403,10 @@ impl BufferViewports {
|
||||
let focused = editor.pane_id();
|
||||
self.activate(focused, editor);
|
||||
self.parked
|
||||
.retain(|id, _| editor.pane_layout().contains(*id));
|
||||
.retain(|id, _| editor.pane_buffer_id(*id).is_some());
|
||||
self.syntax.retain(|id, _| editor.has_buffer(*id));
|
||||
let area = frame.area();
|
||||
let area = draw_tab_bar(frame, editor, area);
|
||||
let multiple = editor.pane_layout().len() > 1;
|
||||
let chrome = if !multiple && editor.diff().is_some() {
|
||||
None
|
||||
@@ -557,8 +558,9 @@ impl BufferViewports {
|
||||
.iter()
|
||||
.chain(
|
||||
self.parked
|
||||
.values()
|
||||
.filter_map(|pane| pane.panels.diff.as_ref()),
|
||||
.iter()
|
||||
.filter(|(id, _)| editor.pane_layout().contains(**id))
|
||||
.filter_map(|(_, pane)| pane.panels.diff.as_ref()),
|
||||
)
|
||||
.any(|geometry| geometry.viewport.is_some());
|
||||
}
|
||||
@@ -573,7 +575,10 @@ impl BufferViewports {
|
||||
{
|
||||
session.set_diff_viewport(comparison_id, file, top, left_before, left_after);
|
||||
}
|
||||
for pane in self.parked.values() {
|
||||
for (pane_id, _) in &self.rects {
|
||||
let Some(pane) = self.parked.get(pane_id) else {
|
||||
continue;
|
||||
};
|
||||
if let Some((id, file, top, before, after)) = pane
|
||||
.panels
|
||||
.diff
|
||||
@@ -909,9 +914,11 @@ fn translate(key: KeyEvent) -> Option<Input> {
|
||||
}
|
||||
if key.modifiers.contains(KeyModifiers::ALT) {
|
||||
return match key.code {
|
||||
KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
Some(Input::Alt(c.to_ascii_lowercase()))
|
||||
}
|
||||
KeyCode::Char(c) => Some(if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||
Input::CtrlAlt(c.to_ascii_lowercase())
|
||||
} else {
|
||||
Input::Alt(c.to_ascii_lowercase())
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
@@ -994,6 +1001,75 @@ fn diagnostic_on_row(diagnostic: &Diagnostic, row: usize) -> bool {
|
||||
|| diagnostic.start == diagnostic.end)
|
||||
}
|
||||
|
||||
fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
|
||||
if editor.tab_count() <= 1 || area.height == 0 {
|
||||
return area;
|
||||
}
|
||||
let bar = Rect::new(area.x, area.y, area.width, 1);
|
||||
let workspace = Rect::new(
|
||||
area.x,
|
||||
area.y.saturating_add(1),
|
||||
area.width,
|
||||
area.height - 1,
|
||||
);
|
||||
if bar.width == 0 {
|
||||
return workspace;
|
||||
}
|
||||
let labels: Vec<_> = (0..editor.tab_count())
|
||||
.map(|index| {
|
||||
let buffer = editor.tab_buffer(index);
|
||||
let name = buffer
|
||||
.as_ref()
|
||||
.and_then(|buffer| buffer.path)
|
||||
.map(|path| {
|
||||
path.file_name()
|
||||
.unwrap_or(path.as_os_str())
|
||||
.to_string_lossy()
|
||||
})
|
||||
.unwrap_or(Cow::Borrowed("[No Name]"));
|
||||
display_text(&format!(
|
||||
"{} {name}{} ",
|
||||
index + 1,
|
||||
if buffer.is_some_and(|buffer| buffer.dirty) {
|
||||
" [+]"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
let active = editor.tab_index();
|
||||
let mut first = active;
|
||||
let mut width = labels[active].width();
|
||||
while first > 0 && width + labels[first - 1].width() <= usize::from(bar.width) {
|
||||
first -= 1;
|
||||
width += labels[first].width();
|
||||
}
|
||||
let inactive = Style::default().fg(Color::Gray).bg(Color::DarkGray);
|
||||
frame.render_widget(Paragraph::new("").style(inactive), bar);
|
||||
let mut x = bar.x;
|
||||
for (index, label) in labels.iter().enumerate().skip(first) {
|
||||
let width = label.width().min(usize::from(bar.right() - x)) as u16;
|
||||
let style = if index == active {
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
inactive
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(label.as_str()).style(style),
|
||||
Rect::new(x, bar.y, width, 1),
|
||||
);
|
||||
x += width;
|
||||
if x == bar.right() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
workspace
|
||||
}
|
||||
|
||||
fn document_layout(area: Rect, editor: &Editor) -> [Rect; 3] {
|
||||
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
|
||||
let dock_focused = editor
|
||||
@@ -1018,6 +1094,7 @@ fn draw(
|
||||
panels: &mut WheelPanels,
|
||||
) {
|
||||
let area = frame.area();
|
||||
let area = draw_tab_bar(frame, editor, area);
|
||||
let areas = if editor.diff().is_some() {
|
||||
[area, Rect::default(), Rect::default()]
|
||||
} else {
|
||||
@@ -1497,6 +1574,7 @@ fn draw_overlays(
|
||||
draw_palette(
|
||||
frame,
|
||||
palette,
|
||||
area,
|
||||
editor.mode(),
|
||||
editor.selection_navigation_hint(),
|
||||
);
|
||||
@@ -3720,8 +3798,13 @@ fn draw_file_list(
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_palette(frame: &mut Frame, palette: &Palette, mode: Mode, navigation_hint: &str) {
|
||||
let area = frame.area();
|
||||
fn draw_palette(
|
||||
frame: &mut Frame,
|
||||
palette: &Palette,
|
||||
area: Rect,
|
||||
mode: Mode,
|
||||
navigation_hint: &str,
|
||||
) {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -5083,6 +5166,212 @@ mod tests {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_bar_shows_shared_dirty_titles_and_reserves_document_and_overlay_rows() {
|
||||
let directory = Directory::new();
|
||||
let mut editor =
|
||||
Editor::open(Some(directory.file("a\tname.txt", "DOCUMENT")), vec![]).unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(views.document.geometry.unwrap().1.y, 0);
|
||||
assert!(pane_screen(&terminal, Rect::new(0, 0, 80, 1)).contains("DOCUMENT"));
|
||||
|
||||
editor.execute("tab.new");
|
||||
editor.handle(Input::Char('i'));
|
||||
editor.handle(Input::Paste("changed ".into()));
|
||||
editor.handle(Input::Escape);
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let titles = pane_screen(&terminal, Rect::new(0, 0, 80, 1));
|
||||
assert!(titles.contains("1 a�name.txt [+]"), "{titles}");
|
||||
assert!(titles.contains("2 a�name.txt [+]"), "{titles}");
|
||||
let active_x = display_column(&titles[..titles.find("2 a�name.txt").unwrap()], usize::MAX);
|
||||
assert_eq!(terminal.backend().buffer()[(0, 0)].bg, Color::DarkGray);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(active_x as u16, 0)].bg,
|
||||
Color::Cyan
|
||||
);
|
||||
assert_eq!(views.document.geometry.unwrap().1.y, 1);
|
||||
assert!(terminal.get_cursor_position().unwrap().y >= 1);
|
||||
assert!(pane_screen(&terminal, views.rects[0].1).contains("changed DOCUMENT"));
|
||||
|
||||
editor.handle(Input::Ctrl('p'));
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(pane_screen(&terminal, Rect::new(0, 0, 80, 1)), titles);
|
||||
assert!(terminal.get_cursor_position().unwrap().y >= 1);
|
||||
assert!(screen_text(&terminal).contains("Commands"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn narrow_tab_bar_keeps_the_active_title_visible_and_handles_empty_areas() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = Editor::open(
|
||||
Some(directory.file("a-very-long-inactive-title.txt", "first")),
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
editor.execute("tab.new");
|
||||
editor.execute("tab.new");
|
||||
editor.execute(&format!(
|
||||
"edit {}",
|
||||
directory.file("active.txt", "ACTIVE").display()
|
||||
));
|
||||
let mut views = BufferViewports::default();
|
||||
for (width, height) in [(12, 9), (7, 9), (1, 9), (0, 9), (12, 1), (12, 0)] {
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
if width > 0 && height > 0 {
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
||||
let expected: String = "3 active.txt".chars().take(usize::from(width)).collect();
|
||||
assert_eq!(title, expected);
|
||||
assert_eq!(terminal.backend().buffer()[(0, 0)].bg, Color::Cyan);
|
||||
}
|
||||
}
|
||||
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.execute("tab.new");
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 6)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let titles = pane_screen(&terminal, Rect::new(0, 0, 40, 1));
|
||||
assert!(titles.contains("1 [No Name]"), "{titles}");
|
||||
assert!(titles.contains("2 [No Name]"), "{titles}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_switches_restore_independent_pane_viewports_and_retire_only_closed_panes() {
|
||||
let directory = Directory::new();
|
||||
let text = (0..80)
|
||||
.map(|row| format!("row-{row:02} {}", "long ".repeat(30)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let mut session =
|
||||
EditorSession::open(Some(directory.file("rows.txt", &text)), vec![], None).unwrap();
|
||||
let buffer = session.editor().buffer_id();
|
||||
session.execute("tab.new");
|
||||
let other = session.editor().pane_id();
|
||||
session.execute("tab.previous");
|
||||
session.execute("pane.split_vertical");
|
||||
let split = session.editor().pane_id();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 24)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
let area = views.rects.iter().find(|(id, _)| *id == split).unwrap().1;
|
||||
assert_eq!(area.y, 1);
|
||||
assert!(!views.scroll(
|
||||
&mut session,
|
||||
document_wheel(MouseEventKind::ScrollDown, 2, 0)
|
||||
));
|
||||
for _ in 0..2 {
|
||||
for kind in [MouseEventKind::ScrollDown, MouseEventKind::ScrollRight] {
|
||||
assert!(views.scroll(&mut session, document_wheel(kind, area.x + 2, area.y + 2)));
|
||||
}
|
||||
}
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
let split_offset = views.offsets[&buffer];
|
||||
assert!(split_offset.0 > 0 && split_offset.1 > 0);
|
||||
|
||||
session.execute("tab.next");
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert_eq!(views.rects.len(), 1);
|
||||
assert_eq!(views.parked[&split].offsets[&buffer], split_offset);
|
||||
for kind in [MouseEventKind::ScrollDown, MouseEventKind::ScrollRight] {
|
||||
assert!(views.scroll(&mut session, document_wheel(kind, 2, 2)));
|
||||
}
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
let other_offset = views.offsets[&buffer];
|
||||
assert_ne!(other_offset, split_offset);
|
||||
|
||||
session.execute("tab.previous");
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert_eq!(session.editor().pane_id(), split);
|
||||
assert_eq!(views.rects.len(), 2);
|
||||
assert_eq!(views.offsets[&buffer], split_offset);
|
||||
assert_eq!(views.parked[&other].offsets[&buffer], other_offset);
|
||||
session.execute("tab.next");
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert_eq!(views.offsets[&buffer], other_offset);
|
||||
session.execute("tab.close");
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert!(!views.parked.contains_key(&other));
|
||||
assert_eq!(views.offsets[&buffer], split_offset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_tab_diff_caches_do_not_poll_colors_or_report_stale_viewports() {
|
||||
let directory = Directory::new();
|
||||
let mut session = EditorSession::open(
|
||||
Some(directory.file("source.txt", "WORKSPACE")),
|
||||
vec![],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let source = (0..80)
|
||||
.map(|row| format!("old-{row:02} {}", "x ".repeat(30)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
install_readonly_diff(
|
||||
session.editor_mut(),
|
||||
syntax_diff_file("history.txt", &source, &source.replace("old", "new")),
|
||||
);
|
||||
let owner = session.editor().pane_id();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 30)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert!(views.diff_visible);
|
||||
let (id, file, _, _, _) = views.panels.diff.as_ref().unwrap().viewport.unwrap();
|
||||
session.set_diff_viewport(id, file, 24, 7, 8);
|
||||
session.execute("tab.new");
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert!(!views.diff_visible);
|
||||
assert!(views.parked[&owner].panels.diff.is_some());
|
||||
assert!(views.rects.iter().all(|(id, _)| *id != owner));
|
||||
|
||||
session.execute("tab.previous");
|
||||
let diff = session.editor().diff().unwrap();
|
||||
assert_eq!((diff.top, diff.left_before, diff.left_after), (24, 7, 8));
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert!(views.diff_visible);
|
||||
assert_eq!(terminal.backend().buffer()[(0, 1)].bg, Color::Cyan);
|
||||
assert_eq!(views.panels.files.unwrap().0.y, 2);
|
||||
|
||||
session.execute("diff");
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert!(views.panels.diff.as_ref().unwrap().regions[0].0.y >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explorer_dock_reserves_space_outside_all_workspace_panes() {
|
||||
let directory = Directory::new();
|
||||
@@ -7566,6 +7855,26 @@ end
|
||||
(
|
||||
KeyCode::Char('j'),
|
||||
KeyModifiers::ALT | KeyModifiers::CONTROL,
|
||||
Some(Input::CtrlAlt('j')),
|
||||
),
|
||||
(
|
||||
KeyCode::Char('H'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT,
|
||||
Some(Input::CtrlAlt('h')),
|
||||
),
|
||||
(
|
||||
KeyCode::Char('L'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT,
|
||||
Some(Input::CtrlAlt('l')),
|
||||
),
|
||||
(
|
||||
KeyCode::Char('j'),
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER,
|
||||
None,
|
||||
),
|
||||
(
|
||||
KeyCode::Left,
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT,
|
||||
None,
|
||||
),
|
||||
(KeyCode::Char('j'), KeyModifiers::SUPER, None),
|
||||
@@ -10567,6 +10876,7 @@ end
|
||||
draw_palette(
|
||||
frame,
|
||||
&palette,
|
||||
area,
|
||||
Mode::Visual,
|
||||
editor.selection_navigation_hint(),
|
||||
)
|
||||
@@ -10605,9 +10915,11 @@ end
|
||||
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
draw_palette(
|
||||
frame,
|
||||
&palette,
|
||||
area,
|
||||
Mode::Normal,
|
||||
editor.selection_navigation_hint(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user