feat: add Git and Jujutsu gutter indicators
This commit is contained in:
+197
-2
@@ -849,6 +849,7 @@ pub fn run(session: &mut EditorSession, mut clear_screen: bool) -> io::Result<()
|
||||
Event::Resize(_, _) => redraw = true,
|
||||
Event::FocusGained => {
|
||||
redraw |= session.refresh_blame();
|
||||
redraw |= session.refresh_gutter();
|
||||
query_theme = true;
|
||||
}
|
||||
Event::ColorSchemeChanged => query_theme = true,
|
||||
@@ -1510,7 +1511,11 @@ fn draw_document(
|
||||
_ => (workspace, None),
|
||||
};
|
||||
let digits = editor.lines().len().ilog10() as usize + 1;
|
||||
let gutter_width = (digits + 1) as u16;
|
||||
let marker_gutter = matches!(
|
||||
editor.gutter_style(),
|
||||
crate::gutter::GutterStyle::Bar | crate::gutter::GutterStyle::Symbols
|
||||
);
|
||||
let gutter_width = (digits + 1 + usize::from(marker_gutter)) as u16;
|
||||
let [gutter, viewport] = Layout::horizontal([
|
||||
Constraint::Length(if viewport.width > gutter_width {
|
||||
gutter_width
|
||||
@@ -1762,7 +1767,40 @@ fn draw_document(
|
||||
} else {
|
||||
row.abs_diff(cursor.row)
|
||||
};
|
||||
Line::styled(format!("{number:>digits$} "), style)
|
||||
let change = editor.gutter_change(row);
|
||||
let color = change.map(|change| match change {
|
||||
crate::gutter::Change::Added => Color::Green,
|
||||
crate::gutter::Change::Removed => Color::Red,
|
||||
crate::gutter::Change::Modified => Color::Yellow,
|
||||
});
|
||||
let style = if editor.gutter_style() == crate::gutter::GutterStyle::LineNumbers
|
||||
{
|
||||
if row == cursor.row {
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
color.map_or(style, |color| style.fg(color))
|
||||
}
|
||||
} else {
|
||||
style
|
||||
};
|
||||
let mut spans = vec![Span::styled(format!("{number:>digits$}"), style)];
|
||||
if marker_gutter {
|
||||
let marker = match (editor.gutter_style(), change) {
|
||||
(crate::gutter::GutterStyle::Bar, Some(_)) => "▌",
|
||||
(_, Some(crate::gutter::Change::Added)) => "+",
|
||||
(_, Some(crate::gutter::Change::Removed)) => "-",
|
||||
(_, Some(crate::gutter::Change::Modified)) => "~",
|
||||
(_, None) => " ",
|
||||
};
|
||||
spans.push(Span::styled(
|
||||
marker,
|
||||
Style::default().fg(color.unwrap_or(Color::DarkGray)),
|
||||
));
|
||||
}
|
||||
spans.push(Span::raw(" "));
|
||||
Line::from(spans)
|
||||
})
|
||||
.collect();
|
||||
frame.render_widget(Paragraph::new(numbers), gutter);
|
||||
@@ -7681,6 +7719,7 @@ end
|
||||
let directory = Directory::new();
|
||||
let mut editor =
|
||||
Editor::open(Some(directory.file("source.txt", &"x".repeat(60))), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor.execute("word_wrap.toggle");
|
||||
let source = editor.blame_source().unwrap();
|
||||
editor.set_line_blame(
|
||||
@@ -9971,6 +10010,7 @@ end
|
||||
CompletionEdit, CompletionItem, CompletionSnippet, SnippetTabstop, TextPosition,
|
||||
};
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor.handle(Input::Char('i'));
|
||||
editor
|
||||
.show_completion(vec![CompletionItem {
|
||||
@@ -10207,6 +10247,7 @@ end
|
||||
let directory = Directory::new();
|
||||
let source = "#a\t界e\u{301}👩🚀z";
|
||||
let mut editor = Editor::open(Some(directory.file("unicode.py", source)), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.replace_diagnostics(
|
||||
"fixture",
|
||||
@@ -10299,6 +10340,7 @@ end
|
||||
#[test]
|
||||
fn diagnostics_render_newlines_anchors_counts_and_messages_without_hiding_core_errors() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"abc\n\nxy\nlast".into(),
|
||||
@@ -10492,6 +10534,9 @@ end
|
||||
Some(&config),
|
||||
)
|
||||
.unwrap();
|
||||
session
|
||||
.editor_mut()
|
||||
.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
let source_id = session.editor().buffer_id();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(18, 6)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
@@ -10600,6 +10645,7 @@ end
|
||||
let directory = Directory::new();
|
||||
let path = directory.file("number.rs", "42");
|
||||
let mut editor = Editor::open(Some(path), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor.set_clipboard_mode("separate").unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
@@ -10729,6 +10775,7 @@ end
|
||||
#[test]
|
||||
fn semantic_roles_remain_distinct_when_values_and_fields_are_readonly() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"ns Type local frozen field fixed param frozenparam".into(),
|
||||
@@ -10785,6 +10832,7 @@ end
|
||||
let directory = Directory::new();
|
||||
let source = "#a\t界e\u{301}👩🚀z";
|
||||
let mut editor = Editor::open(Some(directory.file("unicode.py", source)), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor.set_clipboard_mode("separate").unwrap();
|
||||
editor
|
||||
.replace_semantic_tokens(
|
||||
@@ -10931,6 +10979,7 @@ end
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.replace_semantic_tokens(
|
||||
editor.buffer_id(),
|
||||
@@ -11030,6 +11079,7 @@ end
|
||||
let directory = Directory::new();
|
||||
let mut editor =
|
||||
Editor::open(Some(directory.file("source.rs", "let value = 42;")), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
terminal
|
||||
@@ -11062,6 +11112,7 @@ end
|
||||
fn syntax_caches_stay_independent_across_buffer_edits_and_retire_closed_ids() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = Editor::open(Some(directory.file("a.rs", "42")), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
let a_id = editor.buffer_id();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
@@ -12764,6 +12815,7 @@ end
|
||||
#[test]
|
||||
fn all_dock_sides_clip_editor_gutter_selection_carets_and_cursor() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
vec!["word ".repeat(40); 20].join("\n"),
|
||||
@@ -12864,6 +12916,7 @@ end
|
||||
#[test]
|
||||
fn floating_picker_clears_a_centered_overlay_without_moving_editor_regions() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
vec!["x".repeat(120); 30].join("\n"),
|
||||
@@ -13225,6 +13278,9 @@ end
|
||||
let text = format!("abc\t界e\u{301}{}", "xyz".repeat(20));
|
||||
let path = directory.file("wide.txt", &text);
|
||||
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
|
||||
session
|
||||
.editor_mut()
|
||||
.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
session.handle(Input::Char('v'));
|
||||
session.handle(Input::Char('l'));
|
||||
let cursor = session.editor().cursor();
|
||||
@@ -13557,6 +13613,9 @@ end
|
||||
);
|
||||
let other = directory.file("other.txt", "other");
|
||||
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
|
||||
session
|
||||
.editor_mut()
|
||||
.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
let buffer_id = session.editor().buffer_id();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(30, 6)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
@@ -13672,6 +13731,9 @@ end
|
||||
let directory = Directory::new();
|
||||
let path = directory.file("long.txt", "abcdefghijk");
|
||||
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
|
||||
session
|
||||
.editor_mut()
|
||||
.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
for _ in 0..8 {
|
||||
session.handle(Input::Right);
|
||||
}
|
||||
@@ -13722,6 +13784,7 @@ end
|
||||
let source = "abcdefghijklmnop\ntail";
|
||||
let path = directory.file("wrapped.txt", source);
|
||||
let mut editor = Editor::open(Some(path), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor.execute("word_wrap.toggle");
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(10, 6)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
@@ -13818,6 +13881,7 @@ end
|
||||
let source = format!("#a\t界e\u{301} z\n{}", vec!["# unseen"; 100].join("\n"));
|
||||
let path = directory.file("wrapped.py", &source);
|
||||
let mut editor = Editor::open(Some(path), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.replace_semantic_tokens(
|
||||
editor.buffer_id(),
|
||||
@@ -14017,6 +14081,7 @@ end
|
||||
let a = directory.file("a", &vec!["a".repeat(50); 20].join("\n"));
|
||||
let b = directory.file("b", "other");
|
||||
let mut editor = Editor::open(Some(a), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
let a_id = editor.buffer_id();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(24, 8)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
@@ -14075,6 +14140,7 @@ end
|
||||
let a = directory.file("a", "word\nword\nword");
|
||||
let b = directory.file("b", "other");
|
||||
let mut editor = Editor::open(Some(a), vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
let a_id = editor.buffer_id();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(240, 6)).unwrap();
|
||||
let mut viewports = BufferViewports::default();
|
||||
@@ -14111,6 +14177,7 @@ end
|
||||
#[test]
|
||||
fn pending_motion_count_is_visible_and_escape_cancels_without_moving() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(["row"; 12].join("\n"))])
|
||||
.unwrap();
|
||||
@@ -14155,9 +14222,131 @@ end
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_gutter_modes_colors_geometry_wrap_scroll_and_tiny_panes() {
|
||||
use crate::gutter::{Change, GutterStyle};
|
||||
let directory = Directory::new();
|
||||
let path = directory.file("gutter.txt", "abcdefghijklmnop\nsecond\nthird\nfourth");
|
||||
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||
assert!(editor.set_gutter_changes(
|
||||
editor.buffer_id(),
|
||||
editor.text_revision(),
|
||||
&path,
|
||||
vec![
|
||||
Some(Change::Added),
|
||||
Some(Change::Removed),
|
||||
Some(Change::Modified),
|
||||
None
|
||||
],
|
||||
));
|
||||
for (mode, marker) in [
|
||||
(GutterStyle::Bar, "▌"),
|
||||
(GutterStyle::Symbols, "+"),
|
||||
(GutterStyle::LineNumbers, ""),
|
||||
(GutterStyle::None, ""),
|
||||
] {
|
||||
editor.set_gutter_style(mode);
|
||||
let mut views = BufferViewports::default();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(24, 6)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let viewport = views.panels.caret.unwrap().0;
|
||||
let markers = matches!(mode, GutterStyle::Bar | GutterStyle::Symbols);
|
||||
assert_eq!(viewport.x, if markers { 3 } else { 2 });
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(viewport.x - 1, viewport.y)].symbol(), " ");
|
||||
if markers {
|
||||
assert_eq!(buffer[(1, viewport.y)].symbol(), marker);
|
||||
for (offset, color) in [Color::Green, Color::Red, Color::Yellow]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
assert_eq!(buffer[(1, viewport.y + offset as u16)].fg, color);
|
||||
}
|
||||
if mode == GutterStyle::Symbols {
|
||||
assert_eq!(buffer[(1, viewport.y + 1)].symbol(), "-");
|
||||
assert_eq!(buffer[(1, viewport.y + 2)].symbol(), "~");
|
||||
}
|
||||
} else if mode == GutterStyle::LineNumbers {
|
||||
assert_eq!(buffer[(0, viewport.y)].fg, Color::Cyan);
|
||||
assert_eq!(buffer[(0, viewport.y + 1)].fg, Color::Red);
|
||||
assert_eq!(buffer[(0, viewport.y + 2)].fg, Color::Yellow);
|
||||
}
|
||||
assert_eq!(buffer[(viewport.x, viewport.y)].symbol(), "a");
|
||||
}
|
||||
editor.set_gutter_style(GutterStyle::Symbols);
|
||||
editor.handle(Input::End);
|
||||
let mut scrolled_views = BufferViewports::default();
|
||||
let mut scrolled_terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(8, 4)).unwrap();
|
||||
scrolled_terminal
|
||||
.draw(|frame| scrolled_views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(scrolled_views.offsets[&editor.buffer_id()].1 > 0);
|
||||
let text_area = scrolled_views.panels.caret.unwrap().0;
|
||||
assert_eq!(
|
||||
scrolled_terminal.backend().buffer()[(1, text_area.y)].symbol(),
|
||||
"+"
|
||||
);
|
||||
assert_eq!(text_area.x, 3);
|
||||
editor.handle(Input::Home);
|
||||
editor.execute("word_wrap.toggle");
|
||||
let mut views = BufferViewports::default();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(8, 8)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let viewport = views.panels.caret.unwrap().0;
|
||||
for (screen, &(_, continuation, _)) in views.document.visible.iter().enumerate() {
|
||||
if continuation > 0 {
|
||||
for x in 0..viewport.x {
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(x, viewport.y + screen as u16)].symbol(),
|
||||
" "
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.execute("word_wrap.toggle");
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Down);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(8, 3)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let viewport = views.panels.caret.unwrap().0;
|
||||
for (screen, &(row, _, _)) in views.document.visible.iter().enumerate() {
|
||||
let expected = match row {
|
||||
0 => "+",
|
||||
1 => "-",
|
||||
2 => "~",
|
||||
_ => " ",
|
||||
};
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(1, viewport.y + screen as u16)].symbol(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
for width in [1, 2, 3] {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 3)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(views.panels.caret.unwrap().0.x, 0);
|
||||
}
|
||||
editor.execute("pane.split_vertical");
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(24, 8)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains('~'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gutter_tracks_scrolled_rows_without_scrolling_with_text() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"0123456789界z\n".repeat(11),
|
||||
@@ -14228,6 +14417,7 @@ end
|
||||
#[test]
|
||||
fn gutter_resizes_at_digit_boundaries_and_yields_to_narrow_text() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
let (mut top, mut left) = (0, 0);
|
||||
let mut syntax = SyntaxCache::new();
|
||||
for count in [9usize, 10, 100] {
|
||||
@@ -14727,6 +14917,7 @@ end
|
||||
#[test]
|
||||
fn selection_set_renders_all_ranges_and_counts_offscreen_targets() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"界\n界\n界\n界\n界".into(),
|
||||
@@ -14769,6 +14960,7 @@ end
|
||||
#[test]
|
||||
fn secondary_carets_render_on_empty_lines_and_at_line_end() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace("a\n\n界".into())])
|
||||
.unwrap();
|
||||
@@ -14814,6 +15006,7 @@ end
|
||||
#[test]
|
||||
fn linewise_selection_highlights_copied_text_and_newlines_without_filling_rows() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"\t界e\u{301} \n\n".into(),
|
||||
@@ -14897,6 +15090,7 @@ end
|
||||
#[test]
|
||||
fn visual_selection_highlights_graphemes_newlines_and_clipped_ranges_over_search() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"a\t界e\u{301}z\n\nxy".into(),
|
||||
@@ -15003,6 +15197,7 @@ end
|
||||
#[test]
|
||||
fn active_search_highlight_uses_display_cells_and_clips_with_horizontal_scroll() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"a\t界e\u{301}zz".into(),
|
||||
|
||||
Reference in New Issue
Block a user