feat: configure cursor appearance by editing mode
This commit is contained in:
@@ -1,26 +1,6 @@
|
||||
- [x] improve: conflict resolution view.
|
||||
- two-pane mode:
|
||||
- currently, we display the two panes as if it were a diff, where the left side will be displayed as if the code in question will be removed, and the left pane will display the code in question as if it will be added.
|
||||
- this doesn't really make sense, since code on either side (either version) will be added/removed.
|
||||
- i'm wondering if it would make sense to display in both panels both changes that would and changes that would add code, meaning:
|
||||
- if some code would be removed, it would display in red with a minus
|
||||
- if some code would be added, it would display in green with a plus
|
||||
- this would be the case for both panes
|
||||
- probably something similar for three-pane mode would make sense.
|
||||
- [x] improve: code commenting format. example of comment in current format:
|
||||
<attachment>
|
||||
Comment 14 [unresolved] source/sema/resolve.hon (after) 22:9–22:19 @ working:a3b4159a340b3546e2108bc2dc0e7c302fc6d84b
|
||||
[8]Builtin
|
||||
can't we do `[_]Builtin` here instead for array size inference? if not, this is probably an oversight in the brolang compiler 🤔
|
||||
</attachment>
|
||||
- would probably be nice to instead do this though, if possible, i.e. wrapping the code snippet in question in a codeblock annotated with the right language, e.g.:
|
||||
<attachment>
|
||||
Comment 14 [unresolved] source/sema/resolve.hon (after) 22:9–22:19 @ working:a3b4159a340b3546e2108bc2dc0e7c302fc6d84b
|
||||
```hon
|
||||
[8]Builtin
|
||||
```
|
||||
can't we do `[_]Builtin` here instead for array size inference? if not, this is probably an oversight in the brolang compiler 🤔
|
||||
</attachment>
|
||||
- [x] improve: editing - color the cursor to match the current mode.
|
||||
- enabled by default but should be configurable.
|
||||
- should also be able to configure the cursor character used based on mode (default is just the block cursor as it is now).
|
||||
- [ ] feature: diff view - GitHub code review integration after the local review UX is usable.
|
||||
- publish local reviews explicitly: general PR comments, inline line/range comments, review summaries, approval, and requests for changes.
|
||||
- preserve explicit PR identity and reviewed commit IDs; validate anchors against GitHub's PR diff and recheck the PR head before submission.
|
||||
|
||||
@@ -15,6 +15,8 @@ Vamp.configure do |config|
|
||||
config.clipboard_mode = "system"
|
||||
config.git_blame = true # false disables current-line attribution
|
||||
config.git_gutter = "bar" # or "symbols", "line_numbers", "none"
|
||||
config.cursor = {} # mode colors enabled; all modes use a block cursor
|
||||
# config.cursor = { mode_colors: false, insert: :bar, visual: :underline }
|
||||
config.scroll_distance = 0 # nonnegative edge distance, or nil to center
|
||||
config.scroll_step_lines = nil # Ctrl+u/d: half viewport, or a positive screen-row count
|
||||
config.highlight_trailing_whitespace = false # true marks line-end whitespace in red
|
||||
@@ -58,6 +60,12 @@ Plugin paths resolve relative to the config file, not the shell's working direct
|
||||
`config.file_explorer_side` defaults to `"right"`; other choices are `"left"`, `"top"`, and `"bottom"`. Independent `file_explorer_view` / `file_picker_view` preferences both default to `"tree"` and accept `"list"` as an alternative. Invalid preference strings report notices and retain defaults while valid bindings still install. Non-string/empty values disable the whole configuration like other Ruby type errors.
|
||||
`config.clipboard_mode` accepts `"system"` (default) or `"separate"`. An invalid policy string reports a notice and retains System while valid bindings still install; non-string/empty values reject the whole Ruby configuration.
|
||||
|
||||
`config.cursor` accepts a partial hash. `mode_colors` defaults to `true`: the primary terminal cursor and secondary text carets use cyan in Normal, green in Insert, purple in Visual (including Visual Line), yellow in Command, and orange in Search. Set `config.cursor = { mode_colors: false }` to use the terminal's default cursor color; secondary carets retain their uncolored-mode magenta highlight.
|
||||
|
||||
The `normal`, `insert`, `visual`, `command`, and `search` keys select native terminal cursor shapes: `:block`, `:bar`, or `:underline` (strings also work). Every mode defaults to a steady block. For example, `config.cursor = { insert: :bar, visual: :underline }` changes those two shapes without disabling mode colors. Each assignment replaces the cursor configuration; omitted keys retain built-in defaults. Unknown keys, nonboolean `mode_colors`, unsupported shapes, and non-hash values reject the configuration.
|
||||
|
||||
Shapes apply to the primary hardware cursor, including command/search prompts and historical/diff text; secondary carets remain cell highlights. This uses terminal cursor protocols (OSC 12/112 for color and DECSCUSR for shape), not replacement text glyphs. Unsupported terminals may ignore them. Normal exit, errors, panics, and restart restore the terminal's configured cursor color and shape. No file contents or cursor positions change.
|
||||
|
||||
`config.scroll_distance` defaults to `0`: keep the viewport still until its primary cursor or selected picker entry would leave it. A nonnegative integer reserves that many visible rows above/below the cursor, and terminal-cell columns left/right in unwrapped text. `nil` keeps it centered; integers at least half the current viewport dimension behave the same way until resizing makes that margin usable. The start of content limits centering, while trailing blank space allows centering near the end. Whole two-row picker entries stay aligned, using the nearest entry to the center when exact margins cannot fit.
|
||||
|
||||
The setting applies to ordinary and historical buffers, split/unified diff panes, file pickers and explorers, command/buffer palettes, completion lists, and revision selectors. Wrapped text measures visual rows, not source lines; picker distances use result rows, excluding borders, search fields, and help. Manual mouse-wheel scrolling remains detached until keyboard navigation resumes. Only `nil` or integers from `0` through the platform's Rust `usize` maximum (`18446744073709551615` on 64-bit systems) are accepted; other values reject the configuration.
|
||||
|
||||
@@ -80,6 +80,7 @@ The two skipped shell cleanup checks execute `/bin/ps`, which macOS refuses to l
|
||||
- Status redesign checks cover component-aware home abbreviation without save-path changes; floating command/search submit/cancel, Unicode/tiny/offset clipping and wheel isolation; footer-row reclamation; notification Escape/modal/prefix priority and covered-caret visibility; and ready-server metadata using real framed subprocess fixtures, including stacks, initialization gates, failure, and focus changes.
|
||||
- Unix terminal-hook checks use isolated child processes and real PTYs to keep process-global hooks out of the parallel runner. They cover raw/alternate-screen/mouse-capture cleanup on entry failure, normal return, I/O errors, and panics during frame construction, including original panic-payload propagation.
|
||||
- Redraw checks inspect actual Crossterm output during repeated scrolling: supporting terminals receive synchronized frames, terminals ignoring synchronization never paint with a visible hardware cursor, and the final caret remains visible. A protocol-state regression verifies that the terminal is not left in synchronized-output mode while the next frame is computed.
|
||||
- Cursor checks cover native color/shape output through Normal, Insert, Visual, Command, and Search transitions, independent color disabling and shape overrides, unchanged source text, and guard cleanup on return/error/panic. Live PTY smoke exercises defaults, Insert bar/Visual underline overrides, color-disabled shapes, prompt transitions, and exit restoration; actual appearance depends on terminal support.
|
||||
- Wheel checks render document/panel views and cover three-row scrolling, unchanged carets/selections/text, first/last-page clamping, offscreen cursor hiding, no-op/custom-bound navigation reveal, manual persistence through idle/resize/palette updates, and floating/unfocused-docked explorer selection and focus preservation. Queued-input checks verify that the next frame shows ordered wheel steps without crossing a key or resize boundary.
|
||||
- Queued-navigation checks cover counted ordered motions through short rows, Normal/Visual ranges, remapped press/repeat keys, rendered caret visibility, edit/layout/prefix and non-key barriers, and the 64-key fairness bound. Burst smoke must verify the final cursor position as well as frame count; batching reduces queued redraws without increasing the OS/terminal repeat rate.
|
||||
- Native LSP integration checks launch framed-protocol fixture subprocesses, covering initialization/settings, opt-in startup, synchronization, UTF-16/file-URI conversion, dirty targets, stale requests/diagnostics, and transport failures. Stack checks cover independent initialization, diagnostic merging/clearing, sibling failure isolation in active/parked buffers, same-revision rename/close identity, aggregate limits, first-server position routing, and both shutdowns. Semantic ordering uses protocol/file gates; elapsed deadlines only bound stuck subprocesses.
|
||||
|
||||
@@ -5,6 +5,8 @@ Vamp.configure do |config|
|
||||
config.leader = "space"
|
||||
config.clipboard_mode = "system" # "separate" keeps ordinary y/d/c/p/P internal
|
||||
config.git_blame = true # false disables current-line inline blame
|
||||
config.cursor = {} # mode colors enabled; block cursor in every mode
|
||||
# config.cursor = { mode_colors: false, insert: :bar, visual: :underline }
|
||||
config.scroll_distance = 0 # nonnegative edge distance, or nil to center
|
||||
config.scroll_step_lines = nil # Ctrl+u/d: half viewport, or a positive screen-row count
|
||||
config.highlight_trailing_whitespace = false # true marks line-end whitespace in red
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
use crate::editor::Mode;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub(crate) struct CursorConfig {
|
||||
pub(crate) mode_colors: bool,
|
||||
pub(crate) normal: CursorShape,
|
||||
pub(crate) insert: CursorShape,
|
||||
pub(crate) command: CursorShape,
|
||||
pub(crate) search: CursorShape,
|
||||
pub(crate) visual: CursorShape,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum CursorShape {
|
||||
Block,
|
||||
Bar,
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl Default for CursorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode_colors: true,
|
||||
normal: CursorShape::Block,
|
||||
insert: CursorShape::Block,
|
||||
command: CursorShape::Block,
|
||||
search: CursorShape::Block,
|
||||
visual: CursorShape::Block,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CursorConfig {
|
||||
pub(crate) fn shape(self, mode: Mode) -> CursorShape {
|
||||
match mode {
|
||||
Mode::Normal => self.normal,
|
||||
Mode::Insert => self.insert,
|
||||
Mode::Command => self.command,
|
||||
Mode::Search => self.search,
|
||||
Mode::Visual => self.visual,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2228,6 +2228,7 @@ pub struct Editor {
|
||||
bottom_bar: ChromeLine,
|
||||
popup: PopupConfig,
|
||||
labels: BTreeMap<String, String>,
|
||||
pub(crate) cursor_config: crate::cursor::CursorConfig,
|
||||
scroll_distance: Option<usize>,
|
||||
scroll_step_lines: Option<NonZeroUsize>,
|
||||
highlight_trailing_whitespace: bool,
|
||||
@@ -2353,6 +2354,7 @@ impl Editor {
|
||||
bottom_bar: ChromeLine::bottom(),
|
||||
popup: PopupConfig::default(),
|
||||
labels: BTreeMap::new(),
|
||||
cursor_config: crate::cursor::CursorConfig::default(),
|
||||
scroll_distance: Some(0),
|
||||
scroll_step_lines: None,
|
||||
highlight_trailing_whitespace: false,
|
||||
|
||||
@@ -2,6 +2,7 @@ mod battery;
|
||||
mod blame;
|
||||
pub mod chrome;
|
||||
pub mod clipboard;
|
||||
pub(crate) mod cursor;
|
||||
pub mod diff;
|
||||
pub mod editor;
|
||||
pub mod files;
|
||||
|
||||
@@ -43,6 +43,7 @@ pub struct Config {
|
||||
pub clipboard_mode: String,
|
||||
pub git_blame: bool,
|
||||
pub git_gutter: crate::gutter::GutterStyle,
|
||||
pub(crate) cursor: crate::cursor::CursorConfig,
|
||||
pub top_bar: ChromeLine,
|
||||
pub bottom_bar: ChromeLine,
|
||||
pub popup: PopupConfig,
|
||||
@@ -101,6 +102,7 @@ impl Default for Config {
|
||||
clipboard_mode: "system".into(),
|
||||
git_blame: true,
|
||||
git_gutter: crate::gutter::GutterStyle::default(),
|
||||
cursor: crate::cursor::CursorConfig::default(),
|
||||
top_bar: ChromeLine::top(),
|
||||
bottom_bar: ChromeLine::bottom(),
|
||||
popup: PopupConfig::default(),
|
||||
@@ -146,6 +148,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
clipboard_mode,
|
||||
git_blame,
|
||||
git_gutter,
|
||||
cursor,
|
||||
top_bar,
|
||||
bottom_bar,
|
||||
popup,
|
||||
@@ -192,6 +195,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
config.clipboard_mode = clipboard_mode;
|
||||
config.git_blame = git_blame;
|
||||
config.git_gutter = git_gutter;
|
||||
config.cursor = cursor;
|
||||
config.top_bar = top_bar;
|
||||
config.bottom_bar = bottom_bar;
|
||||
config.popup = popup;
|
||||
@@ -558,6 +562,8 @@ struct ConfigFrame {
|
||||
clipboard_mode: String,
|
||||
git_blame: bool,
|
||||
git_gutter: crate::gutter::GutterStyle,
|
||||
#[serde(default)]
|
||||
cursor: crate::cursor::CursorConfig,
|
||||
#[serde(deserialize_with = "crate::chrome::top")]
|
||||
top_bar: ChromeLine,
|
||||
#[serde(deserialize_with = "crate::chrome::bottom")]
|
||||
|
||||
@@ -142,6 +142,7 @@ module Vamp
|
||||
@clipboard_mode = "system"
|
||||
@git_blame = true
|
||||
@git_gutter = "bar"
|
||||
@cursor = {}
|
||||
@bar = BarConfig.new
|
||||
@popup = PopupConfig.new
|
||||
@labels = {}
|
||||
@@ -231,6 +232,32 @@ module Vamp
|
||||
@git_gutter = style
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => Boolean, String, Symbol}] mode_colors defaults to true; mode shapes default to block
|
||||
# @return [Hash{String => Boolean, String}]
|
||||
def cursor=(options)
|
||||
raise TypeError, "cursor options must be a Hash" unless options.is_a?(Hash)
|
||||
|
||||
@cursor = options.each_with_object({}) do |(option, value), result|
|
||||
option = option.to_s if option.is_a?(Symbol)
|
||||
option = Vamp.string!(option, "cursor option")
|
||||
raise ArgumentError, "duplicate cursor option: #{option}" if result.key?(option)
|
||||
|
||||
case option
|
||||
when "mode_colors"
|
||||
raise TypeError, "cursor mode_colors must be true or false" unless [true, false].include?(value)
|
||||
when "normal", "insert", "command", "search", "visual"
|
||||
value = value.to_s if value.is_a?(Symbol)
|
||||
value = Vamp.string!(value, "cursor shape")
|
||||
unless %w[block bar underline].include?(value)
|
||||
raise ArgumentError, "cursor shape must be block, bar, or underline"
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "unknown cursor option: #{option}"
|
||||
end
|
||||
result[option] = value
|
||||
end
|
||||
end
|
||||
|
||||
# @param values [Hash{String, Symbol => String}]
|
||||
# @return [Hash]
|
||||
def labels=(values)
|
||||
@@ -441,6 +468,7 @@ module Vamp
|
||||
"file_explorer_view" => @file_explorer_view, "clipboard_mode" => @clipboard_mode,
|
||||
"git_blame" => @git_blame,
|
||||
"git_gutter" => @git_gutter,
|
||||
"cursor" => @cursor,
|
||||
"scroll_distance" => @scroll_distance,
|
||||
"scroll_step_lines" => @scroll_step_lines,
|
||||
"highlight_trailing_whitespace" => @highlight_trailing_whitespace,
|
||||
|
||||
@@ -516,6 +516,7 @@ impl EditorSession {
|
||||
}),
|
||||
None => Config::default(),
|
||||
};
|
||||
editor.cursor_config = config.cursor;
|
||||
let (pull_request_preferences_store, mut preferences, notice) = match preferences_path {
|
||||
Some(path) => crate::pull_request_preferences::Store::load(path),
|
||||
None => (
|
||||
|
||||
+79
-6
@@ -14,7 +14,7 @@ use crossterm::event::{
|
||||
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use crossterm::{
|
||||
cursor::Show,
|
||||
cursor::{SetCursorStyle, Show},
|
||||
event::{
|
||||
self, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableFocusChange, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
|
||||
@@ -98,6 +98,7 @@ fn restore_terminal() {
|
||||
DisableMouseCapture,
|
||||
DisableFocusChange,
|
||||
ColorSchemeNotifications(false),
|
||||
ResetCursor,
|
||||
LeaveAlternateScreen,
|
||||
Show
|
||||
);
|
||||
@@ -169,7 +170,8 @@ impl TerminalSession {
|
||||
DisableBracketedPaste,
|
||||
DisableMouseCapture,
|
||||
DisableFocusChange,
|
||||
ColorSchemeNotifications(false)
|
||||
ColorSchemeNotifications(false),
|
||||
ResetCursor
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1290,10 +1292,64 @@ fn wheel_kind(mouse: &MouseEvent) -> Option<MouseEventKind> {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
struct CursorAppearance {
|
||||
shape: SetCursorStyle,
|
||||
color: Option<[u8; 3]>,
|
||||
}
|
||||
|
||||
fn mode_cursor_color(mode: Mode) -> [u8; 3] {
|
||||
match mode {
|
||||
Mode::Normal => [95, 215, 255],
|
||||
Mode::Insert => [135, 215, 135],
|
||||
Mode::Visual => [215, 135, 255],
|
||||
Mode::Command => [255, 215, 95],
|
||||
Mode::Search => [255, 175, 95],
|
||||
}
|
||||
}
|
||||
|
||||
impl CursorAppearance {
|
||||
fn for_editor(editor: &Editor) -> Self {
|
||||
use crate::cursor::CursorShape;
|
||||
Self {
|
||||
shape: match editor.cursor_config.shape(editor.mode()) {
|
||||
CursorShape::Block => SetCursorStyle::SteadyBlock,
|
||||
CursorShape::Bar => SetCursorStyle::SteadyBar,
|
||||
CursorShape::Underline => SetCursorStyle::SteadyUnderScore,
|
||||
},
|
||||
color: editor
|
||||
.cursor_config
|
||||
.mode_colors
|
||||
.then(|| mode_cursor_color(editor.mode())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crossterm::Command for CursorAppearance {
|
||||
fn write_ansi(&self, output: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
self.shape.write_ansi(output)?;
|
||||
if let Some([red, green, blue]) = self.color {
|
||||
write!(output, "\x1b]12;#{red:02x}{green:02x}{blue:02x}\x1b\\")
|
||||
} else {
|
||||
output.write_str("\x1b]112\x1b\\")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResetCursor;
|
||||
|
||||
impl crossterm::Command for ResetCursor {
|
||||
fn write_ansi(&self, output: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
SetCursorStyle::DefaultUserShape.write_ansi(output)?;
|
||||
output.write_str("\x1b]112\x1b\\")
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_frame<W: io::Write>(
|
||||
terminal: &mut Terminal<CrosstermBackend<Vec<u8>>>,
|
||||
output: &mut W,
|
||||
clear_screen: bool,
|
||||
cursor: Option<CursorAppearance>,
|
||||
render: impl FnOnce(&mut Frame),
|
||||
) -> io::Result<()> {
|
||||
// Do not suspend the terminal's presentation while computing the next frame.
|
||||
@@ -1301,6 +1357,9 @@ fn draw_frame<W: io::Write>(
|
||||
crossterm::queue!(terminal.backend_mut(), BeginSynchronizedUpdate)?;
|
||||
// Terminals without synchronized updates still hide the painting cursor.
|
||||
let result = terminal.hide_cursor().and_then(|()| {
|
||||
if let Some(cursor) = cursor {
|
||||
crossterm::queue!(terminal.backend_mut(), cursor)?;
|
||||
}
|
||||
if clear_screen {
|
||||
// Fresh Ratatui buffers are already empty; avoid its blocking cursor query.
|
||||
crossterm::queue!(
|
||||
@@ -1326,6 +1385,7 @@ pub fn run(session: &mut EditorSession, mut clear_screen: bool) -> io::Result<()
|
||||
let mut query_theme = true;
|
||||
let mut redraw = true;
|
||||
let mut pending = None;
|
||||
let mut cursor = None;
|
||||
while !session.editor().should_quit() {
|
||||
redraw |= session.poll();
|
||||
colors.expire(Instant::now());
|
||||
@@ -1334,9 +1394,15 @@ pub fn run(session: &mut EditorSession, mut clear_screen: bool) -> io::Result<()
|
||||
redraw = true;
|
||||
}
|
||||
if redraw {
|
||||
draw_frame(&mut terminal, &mut output, clear_screen, |frame| {
|
||||
viewports.draw_session(frame, session)
|
||||
})?;
|
||||
let appearance = CursorAppearance::for_editor(session.editor());
|
||||
draw_frame(
|
||||
&mut terminal,
|
||||
&mut output,
|
||||
clear_screen,
|
||||
(cursor != Some(appearance)).then_some(appearance),
|
||||
|frame| viewports.draw_session(frame, session),
|
||||
)?;
|
||||
cursor = Some(appearance);
|
||||
redraw = false;
|
||||
clear_screen = false;
|
||||
}
|
||||
@@ -3312,7 +3378,14 @@ fn paint_editor_line(
|
||||
left,
|
||||
column,
|
||||
column.saturating_add(1),
|
||||
Style::default().fg(Color::Black).bg(Color::Magenta),
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(if editor.cursor_config.mode_colors {
|
||||
let [red, green, blue] = mode_cursor_color(editor.mode());
|
||||
Color::Rgb(red, green, blue)
|
||||
} else {
|
||||
Color::Magenta
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,6 +653,69 @@ fn git_gutter_config_modes_default_and_rejection() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_config_partial_shapes_and_invalid_options() {
|
||||
use crate::cursor::CursorShape::{Bar, Block, Underline};
|
||||
|
||||
let fixture = Fixture::new();
|
||||
for (options, mode_colors, shapes) in [
|
||||
(
|
||||
r#"{ insert: :bar, "visual" => "underline" }"#,
|
||||
true,
|
||||
[Block, Bar, Block, Block, Underline],
|
||||
),
|
||||
(
|
||||
r#"{ mode_colors: false, normal: "bar", insert: :underline, command: :bar, search: "underline", visual: :block }"#,
|
||||
false,
|
||||
[Bar, Underline, Bar, Underline, Block],
|
||||
),
|
||||
(
|
||||
r#"{ "mode_colors" => true, normal: :underline, insert: "block", command: "underline", search: :bar, visual: "bar" }"#,
|
||||
true,
|
||||
[Underline, Block, Underline, Bar, Bar],
|
||||
),
|
||||
] {
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
&format!("Vamp.configure {{ |config| config.cursor = {options} }}"),
|
||||
);
|
||||
let cursor = load_config(&path).unwrap().cursor;
|
||||
assert_eq!(cursor.mode_colors, mode_colors);
|
||||
assert_eq!(
|
||||
[
|
||||
cursor.normal,
|
||||
cursor.insert,
|
||||
cursor.command,
|
||||
cursor.search,
|
||||
cursor.visual
|
||||
],
|
||||
shapes
|
||||
);
|
||||
}
|
||||
for options in [
|
||||
"nil",
|
||||
"false",
|
||||
"[]",
|
||||
"{ mode_colors: nil }",
|
||||
"{ mode_colors: 1 }",
|
||||
r#"{ mode_colors: "true" }"#,
|
||||
"{ insert: nil }",
|
||||
"{ insert: false }",
|
||||
"{ insert: 1 }",
|
||||
"{ insert: :beam }",
|
||||
r#"{ normal: "" }"#,
|
||||
"{ unknown: :block }",
|
||||
"{ 1 => :block }",
|
||||
r#"{ insert: :bar, "insert" => :block }"#,
|
||||
] {
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
&format!("Vamp.configure {{ |config| config.cursor = {options} }}"),
|
||||
);
|
||||
assert!(load_config(&path).is_err(), "{options}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_blame_boolean_config_typed_toggle_palette_and_wire_validation() {
|
||||
let fixture = Fixture::new();
|
||||
|
||||
+98
-4
@@ -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,6 +15217,8 @@ 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();
|
||||
for (enabled, expected) in [(true, Color::Rgb(135, 215, 135)), (false, Color::Magenta)] {
|
||||
editor.cursor_config.mode_colors = enabled;
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw(
|
||||
@@ -15232,7 +15237,8 @@ fn secondary_carets_render_on_empty_lines_and_at_line_end() {
|
||||
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);
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user