feat: configure cursor appearance by editing mode

This commit is contained in:
2026-10-26 15:01:19 +01:00
parent 846803a3e2
commit ce434a1c17
13 changed files with 356 additions and 51 deletions
+116 -22
View File
@@ -10908,6 +10908,7 @@ fn search_visual_selection_and_secondary_carets_override_semantic_foreground() {
editor.handle(Input::Char('e'));
editor.handle(Input::Ctrl('a'));
assert_eq!(editor.selection_count(), 3);
editor.cursor_config.mode_colors = false;
terminal
.draw(|frame| viewports.draw(frame, &mut editor))
.unwrap();
@@ -14348,6 +14349,7 @@ fn status_identifies_active_buffer_and_restored_visual_selection_count() {
let b = directory.file("b", "other");
let mut editor = Editor::open(Some(a), vec![]).unwrap();
editor.set_gutter_style(crate::gutter::GutterStyle::None);
editor.cursor_config.mode_colors = false;
let a_id = editor.buffer_id();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(240, 6)).unwrap();
let mut viewports = BufferViewports::default();
@@ -15160,6 +15162,7 @@ fn tabs_and_wide_graphemes_scroll_in_terminal_cells() {
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.cursor_config.mode_colors = false;
editor
.apply(vec![crate::editor::Effect::Replace(
"界\n界\n界\n界\n界".into(),
@@ -15214,25 +15217,28 @@ fn secondary_carets_render_on_empty_lines_and_at_line_end() {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(48, 5)).unwrap();
let (mut top, mut left) = (0, 0);
let mut syntax = SyntaxCache::new();
terminal
.draw(|frame| {
draw(
frame,
&editor,
&mut top,
&mut left,
&mut syntax,
&mut DocumentViewport::default(),
&mut WheelPanels::default(),
)
})
.unwrap();
let buffer = terminal.backend().buffer();
for caret in editor.carets() {
let x = 2 + display_column(&editor.lines()[caret.row], caret.column) as u16;
assert_eq!(buffer[(x, caret.row as u16)].symbol(), " ");
if caret != editor.cursor() {
assert_eq!(buffer[(x, caret.row as u16)].bg, Color::Magenta);
for (enabled, expected) in [(true, Color::Rgb(135, 215, 135)), (false, Color::Magenta)] {
editor.cursor_config.mode_colors = enabled;
terminal
.draw(|frame| {
draw(
frame,
&editor,
&mut top,
&mut left,
&mut syntax,
&mut DocumentViewport::default(),
&mut WheelPanels::default(),
)
})
.unwrap();
let buffer = terminal.backend().buffer();
for caret in editor.carets() {
let x = 2 + display_column(&editor.lines()[caret.row], caret.column) as u16;
assert_eq!(buffer[(x, caret.row as u16)].symbol(), " ");
if caret != editor.cursor() {
assert_eq!(buffer[(x, caret.row as u16)].bg, expected);
}
}
}
assert_eq!(
@@ -15590,6 +15596,87 @@ fn active_search_highlight_uses_display_cells_and_clips_with_horizontal_scroll()
assert!(content.contains(terminal.get_cursor_position().unwrap()));
}
#[test]
fn cursor_appearance_tracks_editing_and_prompt_modes_without_replacing_text() {
use crate::cursor::CursorShape;
let mut editor = Editor::open(None, vec![]).unwrap();
editor
.apply(vec![crate::editor::Effect::Replace("hello\n".into())])
.unwrap();
editor.cursor_config.insert = CursorShape::Bar;
editor.cursor_config.visual = CursorShape::Underline;
let mut terminal = Terminal::with_options(
CrosstermBackend::new(Vec::new()),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 60, 12)),
},
)
.unwrap();
let mut views = BufferViewports::default();
for (inputs, mode, shape, color) in [
(vec![], Mode::Normal, "\x1b[2 q", "#5fd7ff"),
(vec![Input::Char('i')], Mode::Insert, "\x1b[6 q", "#87d787"),
(
vec![Input::Escape, Input::Char('v')],
Mode::Visual,
"\x1b[4 q",
"#d787ff",
),
(
vec![Input::Escape, Input::Char(':')],
Mode::Command,
"\x1b[2 q",
"#ffd75f",
),
(
vec![Input::Escape, Input::Char('/')],
Mode::Search,
"\x1b[2 q",
"#ffaf5f",
),
(vec![Input::Escape], Mode::Normal, "\x1b[2 q", "#5fd7ff"),
] {
for input in inputs {
editor.handle(input);
}
assert_eq!(editor.mode(), mode);
let appearance = CursorAppearance::for_editor(&editor);
let mut output = Vec::new();
draw_frame(
&mut terminal,
&mut output,
false,
Some(appearance),
|frame| views.draw(frame, &mut editor),
)
.unwrap();
let output = String::from_utf8(output).unwrap();
assert!(output.contains(shape), "{mode}: {output:?}");
assert!(
output.contains(&format!("\x1b]12;{color}\x1b\\")),
"{mode}: {output:?}"
);
assert!(output.find(shape).unwrap() < output.find("\x1b[?25h").unwrap());
assert_eq!(editor.lines(), &["hello", ""]);
}
editor.cursor_config.mode_colors = false;
editor.handle(Input::Char('i'));
let mut output = Vec::new();
draw_frame(
&mut terminal,
&mut output,
false,
Some(CursorAppearance::for_editor(&editor)),
|frame| views.draw(frame, &mut editor),
)
.unwrap();
let output = String::from_utf8(output).unwrap();
assert!(output.contains("\x1b[6 q"));
assert!(output.contains("\x1b]112\x1b\\"));
assert!(!output.contains("\x1b]12;"));
}
#[test]
fn terminal_can_present_previous_frame_while_next_frame_is_computed() {
use std::{cell::Cell, rc::Rc};
@@ -15628,7 +15715,7 @@ fn terminal_can_present_previous_frame_while_next_frame_is_computed() {
)
.unwrap();
for text in ["first frame", "second frame", "third frame"] {
draw_frame(&mut terminal, &mut output, false, |frame| {
draw_frame(&mut terminal, &mut output, false, None, |frame| {
assert!(
!synchronized.get(),
"terminal presentation suspended during frame computation"
@@ -15659,7 +15746,7 @@ fn scrolling_never_paints_with_an_exposed_cursor_even_without_synchronized_updat
let mut viewports = BufferViewports::default();
for key in std::iter::repeat_n('j', 20).chain(std::iter::repeat_n('k', 20)) {
editor.handle(Input::Char(key));
draw_frame(&mut terminal, &mut output, false, |frame| {
draw_frame(&mut terminal, &mut output, false, None, |frame| {
viewports.draw(frame, &mut editor)
})
.unwrap();
@@ -15739,7 +15826,7 @@ fn terminal_session_scopes_the_callers_panic_hook() {
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
},
)?;
draw_frame(&mut terminal, &mut io::stdout(), false, |_| {
draw_frame(&mut terminal, &mut io::stdout(), false, None, |_| {
panic::panic_any(73_u64)
})
}
@@ -15866,6 +15953,13 @@ fn terminal_session_scopes_the_callers_panic_hook() {
"{mode}: {before:?}"
);
}
for sequence in ["\x1b[0 q", "\x1b]112\x1b\\"] {
assert_eq!(
before.matches(sequence).count(),
entered,
"{mode}: {before:?}"
);
}
for protocol in [1000, 1002, 1003, 1015, 1006] {
assert_eq!(
before.matches(&format!("\x1b[?{protocol}h")).count(),