Compare commits
3 Commits
846803a3e2
...
496c93774f
| Author | SHA1 | Date | |
|---|---|---|---|
| 496c93774f | |||
| c6901eaaa6 | |||
| ce434a1c17 |
@@ -1,26 +1,3 @@
|
|||||||
- [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>
|
|
||||||
- [ ] feature: diff view - GitHub code review integration after the local review UX is usable.
|
- [ ] 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.
|
- 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.
|
- preserve explicit PR identity and reviewed commit IDs; validate anchors against GitHub's PR diff and recheck the PR head before submission.
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Frontends inspect `file_picker()` for query, presentation, view, explorer-local
|
|||||||
Frontends inspect `diagnostics()` and `diagnostic_at_cursor()` for validated active-buffer annotations. Diagnostics are owned by core LSP or a plugin and buffer revision; text changes clear them. Native server retirement removes only that server's contribution, preserving sibling diagnostics. The TUI underlines annotated text, colors affected gutter numbers by severity, and displays the primary caret's diagnostic message without replacing search/selection backgrounds.
|
Frontends inspect `diagnostics()` and `diagnostic_at_cursor()` for validated active-buffer annotations. Diagnostics are owned by core LSP or a plugin and buffer revision; text changes clear them. Native server retirement removes only that server's contribution, preserving sibling diagnostics. The TUI underlines annotated text, colors affected gutter numbers by severity, and displays the primary caret's diagnostic message without replacing search/selection backgrounds.
|
||||||
Frontends borrow `semantic_tokens()` for sorted, single-line grapheme spans from the active buffer. Immutable `Arc` slices are shared by native LSP and buffer storage; painting does not copy tokens. Text replacements retain tokens outside the changed range and shift following positions using whole graphemes, including newline changes. Tokens overlapping the changed range are dropped; disjoint edits conservatively invalidate the range between their first and last changes. This applies to typing, undo/redo, completion, plugin replacements, and disk reloads. Path changes discard overlays. Native updates validate the whole applicable annotation batch before mutation. The TUI merges semantic styling into its existing glyph pass above syntax foregrounds but below diagnostic/search/selection/caret styling.
|
Frontends borrow `semantic_tokens()` for sorted, single-line grapheme spans from the active buffer. Immutable `Arc` slices are shared by native LSP and buffer storage; painting does not copy tokens. Text replacements retain tokens outside the changed range and shift following positions using whole graphemes, including newline changes. Tokens overlapping the changed range are dropped; disjoint edits conservatively invalidate the range between their first and last changes. This applies to typing, undo/redo, completion, plugin replacements, and disk reloads. Path changes discard overlays. Native updates validate the whole applicable annotation batch before mutation. The TUI merges semantic styling into its existing glyph pass above syntax foregrounds but below diagnostic/search/selection/caret styling.
|
||||||
Frontends inspect `hover()` for optional read-only content and logical-line/grapheme scroll offsets. Popup request eligibility, dismissal, and scrolling stay editor/session-owned; the TUI clips the panel inside the full editor area without changing the primary cursor or viewport. Its hover-local syntax cache treats content as Markdown, including named fenced-code injections, and rebuilds only when content changes. The popup shares the document syntax palette and preserves raw Markdown text and logical scroll coordinates.
|
Frontends inspect `hover()` for optional read-only content and logical-line/grapheme scroll offsets. Popup request eligibility, dismissal, and scrolling stay editor/session-owned; the TUI clips the panel inside the full editor area without changing the primary cursor or viewport. Its hover-local syntax cache treats content as Markdown, including named fenced-code injections, and rebuilds only when content changes. The popup shares the document syntax palette and preserves raw Markdown text and logical scroll coordinates.
|
||||||
Frontends borrow `completion()` for the read-only item list and selected index: zero is Original text, and other indices select `items[selected - 1]`. The editor owns the immutable original document/carets, atomic candidate transactions, cycling/dismissal, pending Insert undo, and final-only repeat recording. It changes only the primary completion target and translates secondary carets through all edits. The TUI places a windowed list near the current caret, bounded by the full editor rather than its pane, sanitizes/clips labels and detail by grapheme, and preserves hardware cursor ownership; wheel input over the list is consumed without dispatching edits.
|
Frontends borrow `completion()` for the read-only item list and selected index: zero is Original text, and other indices select `items[selected - 1]`. The editor owns the immutable original document/carets, atomic candidate transactions, cycling/dismissal, pending Insert undo, and final-only repeat recording. It changes only the primary completion target and translates secondary carets through all edits. The TUI places a windowed list near the current caret, bounded by the full editor rather than its pane, sanitizes/clips labels and detail by grapheme, and preserves logical cursor ownership; wheel input over the list is consumed without dispatching edits.
|
||||||
Frontends borrow `snippet_selections()` for the active argument's end-exclusive grapheme ranges, including mirrors and empty tabstops. The editor caches those ranges after previews, argument edits, and navigation; rendering adds no snippet allocation. Template edits stay primary-only, translate other carets, and fold resolved replacement text and caret placement into the pending completion repeat step. Per-occurrence ancestry distinguishes nested arguments from adjacent empty stops, allowing enclosing replacements to retire only overwritten descendants.
|
Frontends borrow `snippet_selections()` for the active argument's end-exclusive grapheme ranges, including mirrors and empty tabstops. The editor caches those ranges after previews, argument edits, and navigation; rendering adds no snippet allocation. Template edits stay primary-only, translate other carets, and fold resolved replacement text and caret placement into the pending completion repeat step. Per-occurrence ancestry distinguishes nested arguments from adjacent empty stops, allowing enclosing replacements to retire only overwritten descendants.
|
||||||
|
|
||||||
## Terminal lifecycle and layout
|
## Terminal lifecycle and layout
|
||||||
@@ -74,13 +74,13 @@ The terminal guard owns raw mode, alternate-screen state, bracketed paste, mouse
|
|||||||
|
|
||||||
`tui::TerminalSession::run(inherited_screen, body)` scopes this ownership; `tui::run(&mut session, inherited_screen)` renders inside that active guard. On Unix, a fresh alternate screen pushes disambiguated keyboard flags so Ctrl+i stays distinct from Tab; final cleanup pops them. `TerminalSession::prepare_restart()` resets process-local input state without leaving the alternate screen or popping its keyboard flags. The replacement process inherits that stack entry rather than pushing another, and the guard stays armed if relaunch fails.
|
`tui::TerminalSession::run(inherited_screen, body)` scopes this ownership; `tui::run(&mut session, inherited_screen)` renders inside that active guard. On Unix, a fresh alternate screen pushes disambiguated keyboard flags so Ctrl+i stays distinct from Tab; final cleanup pops them. `TerminalSession::prepare_restart()` resets process-local input state without leaving the alternate screen or popping its keyboard flags. The replacement process inherits that stack entry rather than pushing another, and the guard stays armed if relaunch fails.
|
||||||
|
|
||||||
Redraws stage Ratatui's buffer diff, cursor placement, and DEC synchronized-update commands (mode 2026) in a reusable byte buffer, then write and flush the complete frame to the terminal. Frame computation never suspends terminal presentation; synchronization covers transmitted painting rather than layout/highlighting work. The hardware cursor is hidden before painting even when synchronization is unsupported. Terminal cleanup releases synchronization before leaving the alternate screen or invoking caller panic diagnostics. Backend-writer access is enabled solely for this staging seam.
|
Redraws stage Ratatui's buffer diff, logical cursor placement, and DEC synchronized-update commands (mode 2026) in a reusable byte buffer, then write and flush the complete frame to the terminal. Frame computation never suspends terminal presentation; synchronization covers transmitted painting rather than layout/highlighting work. `WheelPanels::cursor` records the final focused cursor position; the software cursor is painted after overlays, using the same named ANSI mode palette as the bars. `SoftwareCursorBackend` delegates drawing and cursor coordinates but suppresses hardware-cursor visibility, retaining input-method placement without a second cursor or OSC 12 dependency. The hardware cursor stays hidden even when synchronization is unsupported. Terminal cleanup releases synchronization before leaving the alternate screen, then restores cursor visibility before returning or invoking caller panic diagnostics. Backend-writer access is enabled solely for this staging seam.
|
||||||
|
|
||||||
`terminal_colors` intercepts crossterm's key-shaped OSC 11/4 replies before editor dispatch, assembles a bounded RGB background/red/green triple, and preserves unrelated events and opaque bracketed paste. Queries never block input and start after painting, so syntax initialization does not spend their reply deadline. Startup, color-scheme notifications, and focus-regain queries replace cached diff tints atomically. While any rendered pane shows a diff, a one-second fallback probes for silent palette changes or missed notifications; ordinary editor views do not poll. Its injected-time deadline prevents overlapping periodic requests. An unanswered/partial query expires after 250 ms and removes stale tints rather than assuming a dark theme. Valid late replies are consumed without becoming editing commands. The frontend precomputes rounded RGB blends from the original background (18% row, 38% character), propagating them to active, parked, and newly created panes.
|
`terminal_colors` intercepts crossterm's key-shaped OSC 11/4 replies before editor dispatch, assembles a bounded RGB background/red/green triple, and preserves unrelated events and opaque bracketed paste. Queries never block input and start after painting, so syntax initialization does not spend their reply deadline. Startup, color-scheme notifications, and focus-regain queries replace cached diff tints atomically. While any rendered pane shows a diff, a one-second fallback probes for silent palette changes or missed notifications; ordinary editor views do not poll. Its injected-time deadline prevents overlapping periodic requests. An unanswered/partial query expires after 250 ms and removes stale tints rather than assuming a dark theme. Valid late replies are consumed without becoming editing commands. The frontend precomputes rounded RGB blends from the original background (18% row, 38% character), propagating them to active, parked, and newly created panes.
|
||||||
|
|
||||||
`vendor/crossterm` retains Crossterm 0.29.0's upstream source and MIT license, with a small Unix decoder patch exposing `Event::ColorSchemeChanged` for `CSI ? 997 ; 1/2 n`. The report signals a palette refresh, not a choice of hard-coded light/dark colors. Valid fragmented reports and malformed color reports leave following keys intact. Cargo patches both direct and Ratatui dependency paths to this one copy; remove the patch/workspace member once upstream supports these notifications.
|
`vendor/crossterm` retains Crossterm 0.29.0's upstream source and MIT license, with a small Unix decoder patch exposing `Event::ColorSchemeChanged` for `CSI ? 997 ; 1/2 n`. The report signals a palette refresh, not a choice of hard-coded light/dark colors. Valid fragmented reports and malformed color reports leave following keys intact. Cargo patches both direct and Ratatui dependency paths to this one copy; remove the patch/workspace member once upstream supports these notifications.
|
||||||
|
|
||||||
The TUI allocates a flexible buffer viewport and optional one-row top/bottom bars, with no reserved prompt/message footer. Both bars derive visibility from configured content: empty content takes no row, and tabs-only content is hidden with a single tab in either position. Both render across ordinary, split, diff, and selector views. Ordered left/center/right groups use full-row centering, active-tab collision priority, internal padding, and independent styled edges. Floating Command/Search input owns the hardware cursor; non-focusable operation/diagnostic notifications anchor bottom-right inside the chrome-free workspace and hide obscured document carets. Higher-priority modal overlays suppress them without changing input state. Wheel hit regions clear each frame and consume reports over these popups. Zero-sized terminals render nothing; minimal-height input never reserves another row. Layout and terminal-cell text mapping remain frontend-owned.
|
The TUI allocates a flexible buffer viewport and optional one-row top/bottom bars, with no reserved prompt/message footer. Both bars derive visibility from configured content: empty content takes no row, and tabs-only content is hidden with a single tab in either position. Both render across ordinary, split, diff, and selector views. Ordered left/center/right groups use full-row centering, active-tab collision priority, internal padding, and independent styled edges; accented content and edges share the focused mode's ANSI palette colors. Floating Command/Search input owns the software cursor; non-focusable operation/diagnostic notifications anchor bottom-right inside the chrome-free workspace and hide obscured document carets. Higher-priority modal overlays suppress them without changing input state. Wheel hit regions clear each frame and consume reports over these popups. Zero-sized terminals render nothing; minimal-height input never reserves another row. Layout and terminal-cell text mapping remain frontend-owned.
|
||||||
|
|
||||||
Comparison views reserve no separate metadata header or native-buffer help footer. Revision labels render bottom-left in pane borders (combined on single-pane views), with per-side editability top-right and focus indicated by border styling. Dock and document geometry use the reclaimed workspace rows; conditional loading/error/file notices remain separate from the border metadata.
|
Comparison views reserve no separate metadata header or native-buffer help footer. Revision labels render bottom-left in pane borders (combined on single-pane views), with per-side editability top-right and focus indicated by border styling. Dock and document geometry use the reclaimed workspace rows; conditional loading/error/file notices remain separate from the border metadata.
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Vamp.configure do |config|
|
|||||||
config.clipboard_mode = "system"
|
config.clipboard_mode = "system"
|
||||||
config.git_blame = true # false disables current-line attribution
|
config.git_blame = true # false disables current-line attribution
|
||||||
config.git_gutter = "bar" # or "symbols", "line_numbers", "none"
|
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_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.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
|
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.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.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 software cursor and secondary text carets use the terminal's ANSI cyan in Normal, green in Insert, magenta in Visual (including Visual Line), yellow in Command, and blue in Search. These are palette entries from the current terminal color scheme, not fixed RGB colors. Set `config.cursor = { mode_colors: false }` for a neutral reverse-video block cursor (default text color for bar/underline); secondary carets retain their magenta highlight.
|
||||||
|
|
||||||
|
The `normal`, `insert`, `visual`, `command`, and `search` keys select software cursor shapes: `:block`, `:bar`, or `:underline` (strings also work). Every mode defaults to a steady block. Block and underline preserve the displayed grapheme; the bar draws `▏` over the hovered character until the cursor moves. 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 cursor, including command/search prompts, pickers, and historical/diff text; secondary carets remain cell highlights. The cursor is painted after the focused view's overlays, takes precedence over syntax/search/selection colors, and disappears when its position is offscreen or covered by a notification. The hardware cursor stays hidden during editing; its logical position is retained for input methods, and normal exit/errors/panics restore its visibility. Vamp does not change terminal cursor color or shape settings, so multiplexers need no native cursor-color support. File contents and cursor positions remain unchanged.
|
||||||
|
|
||||||
`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.
|
`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.
|
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.
|
||||||
@@ -135,6 +143,8 @@ config.bind "leader s", config.core.diff.orientation.toggle
|
|||||||
|
|
||||||
`config.bar.top` and `config.bar.bottom` each accept ordered `left`, `center`, and `right` arrays of segment names. Both bars accept every builtin and custom segment, with the same anchors, title templates, margins, padding, styled edges, and collision handling. Left and right groups anchor to their respective edges; the center group is centered on the whole row, not on the gap remaining between the other groups. Array order is display order within each group. Omitted groups and segment options retain that bar's defaults; an empty array removes that group.
|
`config.bar.top` and `config.bar.bottom` each accept ordered `left`, `center`, and `right` arrays of segment names. Both bars accept every builtin and custom segment, with the same anchors, title templates, margins, padding, styled edges, and collision handling. Left and right groups anchor to their respective edges; the center group is centered on the whole row, not on the gap remaining between the other groups. Array order is display order within each group. Omitted groups and segment options retain that bar's defaults; an empty array removes that group.
|
||||||
|
|
||||||
|
Accented segments (including active tabs, mode, position, and custom labels), their padding, and decorative edge colors follow the focused editing mode using the same ANSI palette as the software cursor. Both bars change together; inactive tabs, neutral text, separators, and margins retain their existing styling. Setting `config.cursor = { mode_colors: false }` affects cursors only, not the bars.
|
||||||
|
|
||||||
The renderer inserts one unstyled blank column between visible items in each group, including tab titles. Hidden items contribute no separator; there is no separator before the first or after the last item. Separator widths count toward anchoring and collision handling. Plain-text segments default to no padding or margins, so adjacent labels do not double their spacing.
|
The renderer inserts one unstyled blank column between visible items in each group, including tab titles. Hidden items contribute no separator; there is no separator before the first or after the last item. Separator widths count toward anchoring and collision handling. Plain-text segments default to no padding or margins, so adjacent labels do not double their spacing.
|
||||||
|
|
||||||
Use the two setters independently; a combined `config.bar = { ... }` assignment is not supported.
|
Use the two setters independently; a combined `config.bar = { ... }` assignment is not supported.
|
||||||
|
|||||||
+2
-1
@@ -79,7 +79,8 @@ The two skipped shell cleanup checks execute `/bin/ps`, which macOS refuses to l
|
|||||||
- Battery checks inject samples and refresh gates, use temporary Linux sysfs fixtures, and validate native value types/bounds without depending on the machine's charge or clock. Unit sessions do not automatically read hardware. Live smoke should compare the default indicator with OS telemetry and observe a production refresh; no-battery, explicit overrides, and disabled anchors must not fabricate values.
|
- Battery checks inject samples and refresh gates, use temporary Linux sysfs fixtures, and validate native value types/bounds without depending on the machine's charge or clock. Unit sessions do not automatically read hardware. Live smoke should compare the default indicator with OS telemetry and observe a production refresh; no-battery, explicit overrides, and disabled anchors must not fabricate values.
|
||||||
- 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.
|
- 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.
|
- 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.
|
- 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 software 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 render software block/bar/underline shapes, wide/combining graphemes, cell restoration after motion, neutral reverse-video blocks, and shared ANSI mode accents on both banners through Normal, Insert, Visual, Command, and Search. The production backend preserves logical cursor coordinates without showing the hardware cursor. Live PTY screen parsing and an isolated Herdr 0.9.5 full-client smoke verify that mode-colored cells survive multiplexing, match both banners, avoid OSC 12, leave source bytes unchanged, and restore hardware visibility on exit. Document-layer fixtures exclude the final cursor overlay; complete-frame checks use `BufferViewports` with the production backend adapter.
|
||||||
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
+2
-2
@@ -74,7 +74,7 @@ With an enabled server that provides completion, Ctrl+n in Insert requests candi
|
|||||||
|
|
||||||
Every candidate is applied from that same immutable original document, not on top of the previous candidate. A candidate's primary replacement and additional text edits are one atomic change. Other keys close the list without accepting it: plain-text completion retains ordinary typing, newline, and configured Tab insertion; snippet completion keeps its argument navigation described below. Escape returns to Normal retaining the displayed text. There is no automatic popup. Empty, unsupported, or invalid results leave the document unchanged and report a message.
|
Every candidate is applied from that same immutable original document, not on top of the previous candidate. A candidate's primary replacement and additional text edits are one atomic change. Other keys close the list without accepting it: plain-text completion retains ordinary typing, newline, and configured Tab insertion; snippet completion keeps its argument navigation described below. Escape returns to Normal retaining the displayed text. There is no automatic popup. Empty, unsupported, or invalid results leave the document unchanged and report a message.
|
||||||
|
|
||||||
Cycling creates no separate undo entries. Only the final chosen edits join the existing Insert undo group; choosing Original text adds no completion edit. Undo restores that group, and dot repeats the final resolved completion edits rather than requests or intermediate candidates. The list stays inside the document text workspace, does not cover the gutter or explorer, and never takes the hardware cursor. Wheel input over it is consumed without choosing a candidate.
|
Cycling creates no separate undo entries. Only the final chosen edits join the existing Insert undo group; choosing Original text adds no completion edit. Undo restores that group, and dot repeats the final resolved completion edits rather than requests or intermediate candidates. The list stays inside the document text workspace, does not cover the gutter or explorer, and never takes the primary cursor. Wheel input over it is consumed without choosing a candidate.
|
||||||
|
|
||||||
Snippet candidates expand the full template immediately, including parentheses and argument defaults. The first numbered argument is highlighted; typing replaces it, then continues within that argument. Repeated occurrences of the same argument update together, without broadcasting to secondary carets. Backspace/Delete stay within the active argument; Enter can insert a newline there. **Tab** moves forward and **Shift+Tab** backward in ascending tabstop order. Replacing an enclosing argument removes its nested arguments, not adjacent independent ones. Reaching the final `$0` position ends navigation, so subsequent Tab uses the configured spaces-or-tab insertion. Escape, ordinary cursor motion, paste, commands, and focus/buffer changes also end navigation without removing the expanded text.
|
Snippet candidates expand the full template immediately, including parentheses and argument defaults. The first numbered argument is highlighted; typing replaces it, then continues within that argument. Repeated occurrences of the same argument update together, without broadcasting to secondary carets. Backspace/Delete stay within the active argument; Enter can insert a newline there. **Tab** moves forward and **Shift+Tab** backward in ascending tabstop order. Replacing an enclosing argument removes its nested arguments, not adjacent independent ones. Reaching the final `$0` position ends navigation, so subsequent Tab uses the configured spaces-or-tab insertion. Escape, ordinary cursor motion, paste, commands, and focus/buffer changes also end navigation without removing the expanded text.
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ System clipboard publication must succeed before any deletion. Failure preserves
|
|||||||
|
|
||||||
## Selections and multi-caret editing
|
## Selections and multi-caret editing
|
||||||
|
|
||||||
Editing uses an ordered selection set with one primary target. Cursor and word motions apply to every target; Visual motions keep each target's anchor and move its active endpoint. Whole combining sequences and ZWJ emoji remain intact. `v` selects characterwise, inclusive of endpoints; `V` selects every complete row touched by the endpoints, including blank and empty final rows. Switching between `v` and `V` retains endpoints; pressing the current shape's selector again cancels. Read-only ranges are end-exclusive. Characterwise `$`/End can select the newline after a non-final row; selected newlines appear as a blue blank cell after the line. Linewise highlighting covers the copied text, including trailing whitespace, plus one blank cell for each copied newline; unused viewport space and the gutter remain unhighlighted. The status reads VISUAL LINE. Blue highlighting overrides yellow search highlighting; secondary carets are magenta, including on empty lines and at line ends. Only the primary owns the hardware cursor and viewport. The status line counts all targets, including offscreen targets.
|
Editing uses an ordered selection set with one primary target. Cursor and word motions apply to every target; Visual motions keep each target's anchor and move its active endpoint. Whole combining sequences and ZWJ emoji remain intact. `v` selects characterwise, inclusive of endpoints; `V` selects every complete row touched by the endpoints, including blank and empty final rows. Switching between `v` and `V` retains endpoints; pressing the current shape's selector again cancels. Read-only ranges are end-exclusive. Characterwise `$`/End can select the newline after a non-final row; selected newlines appear as a blue blank cell after the line. Linewise highlighting covers the copied text, including trailing whitespace, plus one blank cell for each copied newline; unused viewport space and the gutter remain unhighlighted. The status reads VISUAL LINE. Blue highlighting overrides yellow search highlighting; secondary carets follow the mode palette (magenta in Visual), including on empty lines and at line ends. The primary software cursor overlays these text colors and alone drives the viewport; its shape and mode colors are [configurable](configuration.md#preferences). The status line counts all targets, including offscreen targets.
|
||||||
|
|
||||||
Ctrl+k/Ctrl+e clone each target to the adjacent row, preserving preferred terminal-cell columns (tabs use four-cell stops); Visual cloning preserves both endpoints. Duplicate carets and truly overlapping selections merge; adjacent ranges remain distinct. Visual Ctrl+l creates one Normal caret at the first nonblank of each touched row. Visual `I`/`A` similarly create one Insert caret per touched row at first nonblank/end. Occurrence actions use the primary selection as a literal, potentially multiline, grapheme-aligned query: Ctrl+n adds the next non-overlapping occurrence and wraps; Ctrl+s moves only the primary to that occurrence; Ctrl+a selects all occurrences.
|
Ctrl+k/Ctrl+e clone each target to the adjacent row, preserving preferred terminal-cell columns (tabs use four-cell stops); Visual cloning preserves both endpoints. Duplicate carets and truly overlapping selections merge; adjacent ranges remain distinct. Visual Ctrl+l creates one Normal caret at the first nonblank of each touched row. Visual `I`/`A` similarly create one Insert caret per touched row at first nonblank/end. Occurrence actions use the primary selection as a literal, potentially multiline, grapheme-aligned query: Ctrl+n adds the next non-overlapping occurrence and wraps; Ctrl+s moves only the primary to that occurrence; Ctrl+a selects all occurrences.
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ Vamp.configure do |config|
|
|||||||
config.leader = "space"
|
config.leader = "space"
|
||||||
config.clipboard_mode = "system" # "separate" keeps ordinary y/d/c/p/P internal
|
config.clipboard_mode = "system" # "separate" keeps ordinary y/d/c/p/P internal
|
||||||
config.git_blame = true # false disables current-line inline blame
|
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_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.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
|
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,
|
bottom_bar: ChromeLine,
|
||||||
popup: PopupConfig,
|
popup: PopupConfig,
|
||||||
labels: BTreeMap<String, String>,
|
labels: BTreeMap<String, String>,
|
||||||
|
pub(crate) cursor_config: crate::cursor::CursorConfig,
|
||||||
scroll_distance: Option<usize>,
|
scroll_distance: Option<usize>,
|
||||||
scroll_step_lines: Option<NonZeroUsize>,
|
scroll_step_lines: Option<NonZeroUsize>,
|
||||||
highlight_trailing_whitespace: bool,
|
highlight_trailing_whitespace: bool,
|
||||||
@@ -2353,6 +2354,7 @@ impl Editor {
|
|||||||
bottom_bar: ChromeLine::bottom(),
|
bottom_bar: ChromeLine::bottom(),
|
||||||
popup: PopupConfig::default(),
|
popup: PopupConfig::default(),
|
||||||
labels: BTreeMap::new(),
|
labels: BTreeMap::new(),
|
||||||
|
cursor_config: crate::cursor::CursorConfig::default(),
|
||||||
scroll_distance: Some(0),
|
scroll_distance: Some(0),
|
||||||
scroll_step_lines: None,
|
scroll_step_lines: None,
|
||||||
highlight_trailing_whitespace: false,
|
highlight_trailing_whitespace: false,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod battery;
|
|||||||
mod blame;
|
mod blame;
|
||||||
pub mod chrome;
|
pub mod chrome;
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
|
pub(crate) mod cursor;
|
||||||
pub mod diff;
|
pub mod diff;
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ pub struct Config {
|
|||||||
pub clipboard_mode: String,
|
pub clipboard_mode: String,
|
||||||
pub git_blame: bool,
|
pub git_blame: bool,
|
||||||
pub git_gutter: crate::gutter::GutterStyle,
|
pub git_gutter: crate::gutter::GutterStyle,
|
||||||
|
pub(crate) cursor: crate::cursor::CursorConfig,
|
||||||
pub top_bar: ChromeLine,
|
pub top_bar: ChromeLine,
|
||||||
pub bottom_bar: ChromeLine,
|
pub bottom_bar: ChromeLine,
|
||||||
pub popup: PopupConfig,
|
pub popup: PopupConfig,
|
||||||
@@ -101,6 +102,7 @@ impl Default for Config {
|
|||||||
clipboard_mode: "system".into(),
|
clipboard_mode: "system".into(),
|
||||||
git_blame: true,
|
git_blame: true,
|
||||||
git_gutter: crate::gutter::GutterStyle::default(),
|
git_gutter: crate::gutter::GutterStyle::default(),
|
||||||
|
cursor: crate::cursor::CursorConfig::default(),
|
||||||
top_bar: ChromeLine::top(),
|
top_bar: ChromeLine::top(),
|
||||||
bottom_bar: ChromeLine::bottom(),
|
bottom_bar: ChromeLine::bottom(),
|
||||||
popup: PopupConfig::default(),
|
popup: PopupConfig::default(),
|
||||||
@@ -146,6 +148,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
|||||||
clipboard_mode,
|
clipboard_mode,
|
||||||
git_blame,
|
git_blame,
|
||||||
git_gutter,
|
git_gutter,
|
||||||
|
cursor,
|
||||||
top_bar,
|
top_bar,
|
||||||
bottom_bar,
|
bottom_bar,
|
||||||
popup,
|
popup,
|
||||||
@@ -192,6 +195,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
|||||||
config.clipboard_mode = clipboard_mode;
|
config.clipboard_mode = clipboard_mode;
|
||||||
config.git_blame = git_blame;
|
config.git_blame = git_blame;
|
||||||
config.git_gutter = git_gutter;
|
config.git_gutter = git_gutter;
|
||||||
|
config.cursor = cursor;
|
||||||
config.top_bar = top_bar;
|
config.top_bar = top_bar;
|
||||||
config.bottom_bar = bottom_bar;
|
config.bottom_bar = bottom_bar;
|
||||||
config.popup = popup;
|
config.popup = popup;
|
||||||
@@ -558,6 +562,8 @@ struct ConfigFrame {
|
|||||||
clipboard_mode: String,
|
clipboard_mode: String,
|
||||||
git_blame: bool,
|
git_blame: bool,
|
||||||
git_gutter: crate::gutter::GutterStyle,
|
git_gutter: crate::gutter::GutterStyle,
|
||||||
|
#[serde(default)]
|
||||||
|
cursor: crate::cursor::CursorConfig,
|
||||||
#[serde(deserialize_with = "crate::chrome::top")]
|
#[serde(deserialize_with = "crate::chrome::top")]
|
||||||
top_bar: ChromeLine,
|
top_bar: ChromeLine,
|
||||||
#[serde(deserialize_with = "crate::chrome::bottom")]
|
#[serde(deserialize_with = "crate::chrome::bottom")]
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ module Vamp
|
|||||||
@clipboard_mode = "system"
|
@clipboard_mode = "system"
|
||||||
@git_blame = true
|
@git_blame = true
|
||||||
@git_gutter = "bar"
|
@git_gutter = "bar"
|
||||||
|
@cursor = {}
|
||||||
@bar = BarConfig.new
|
@bar = BarConfig.new
|
||||||
@popup = PopupConfig.new
|
@popup = PopupConfig.new
|
||||||
@labels = {}
|
@labels = {}
|
||||||
@@ -231,6 +232,32 @@ module Vamp
|
|||||||
@git_gutter = style
|
@git_gutter = style
|
||||||
end
|
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}]
|
# @param values [Hash{String, Symbol => String}]
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def labels=(values)
|
def labels=(values)
|
||||||
@@ -441,6 +468,7 @@ module Vamp
|
|||||||
"file_explorer_view" => @file_explorer_view, "clipboard_mode" => @clipboard_mode,
|
"file_explorer_view" => @file_explorer_view, "clipboard_mode" => @clipboard_mode,
|
||||||
"git_blame" => @git_blame,
|
"git_blame" => @git_blame,
|
||||||
"git_gutter" => @git_gutter,
|
"git_gutter" => @git_gutter,
|
||||||
|
"cursor" => @cursor,
|
||||||
"scroll_distance" => @scroll_distance,
|
"scroll_distance" => @scroll_distance,
|
||||||
"scroll_step_lines" => @scroll_step_lines,
|
"scroll_step_lines" => @scroll_step_lines,
|
||||||
"highlight_trailing_whitespace" => @highlight_trailing_whitespace,
|
"highlight_trailing_whitespace" => @highlight_trailing_whitespace,
|
||||||
|
|||||||
@@ -516,6 +516,7 @@ impl EditorSession {
|
|||||||
}),
|
}),
|
||||||
None => Config::default(),
|
None => Config::default(),
|
||||||
};
|
};
|
||||||
|
editor.cursor_config = config.cursor;
|
||||||
let (pull_request_preferences_store, mut preferences, notice) = match preferences_path {
|
let (pull_request_preferences_store, mut preferences, notice) = match preferences_path {
|
||||||
Some(path) => crate::pull_request_preferences::Store::load(path),
|
Some(path) => crate::pull_request_preferences::Store::load(path),
|
||||||
None => (
|
None => (
|
||||||
|
|||||||
+185
-31
@@ -28,8 +28,9 @@ use crossterm::{
|
|||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame, Terminal,
|
Frame, Terminal,
|
||||||
backend::CrosstermBackend,
|
backend::{Backend, ClearType, CrosstermBackend, WindowSize},
|
||||||
layout::{Constraint, Layout, Position, Rect},
|
buffer::Cell,
|
||||||
|
layout::{Constraint, Layout, Position, Rect, Size},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
widgets::{Block, Borders, Clear, Paragraph, Wrap},
|
||||||
@@ -837,6 +838,7 @@ impl BufferViewports {
|
|||||||
.filter_map(|(_, pane)| pane.panels.diff.as_ref()),
|
.filter_map(|(_, pane)| pane.panels.diff.as_ref()),
|
||||||
)
|
)
|
||||||
.any(|geometry| geometry.viewport.is_some());
|
.any(|geometry| geometry.viewport.is_some());
|
||||||
|
paint_cursor(frame, editor, self.panels.cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_resolver_history(
|
fn draw_resolver_history(
|
||||||
@@ -1290,29 +1292,149 @@ fn wheel_kind(mouse: &MouseEvent) -> Option<MouseEventKind> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mode_accent(mode: Mode) -> Color {
|
||||||
|
match mode {
|
||||||
|
Mode::Normal => Color::Cyan,
|
||||||
|
Mode::Insert => Color::Green,
|
||||||
|
Mode::Visual => Color::Magenta,
|
||||||
|
Mode::Command => Color::Yellow,
|
||||||
|
Mode::Search => Color::Blue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mode_style(mode: Mode) -> Style {
|
||||||
|
Style::default()
|
||||||
|
.fg(if mode == Mode::Search {
|
||||||
|
Color::White
|
||||||
|
} else {
|
||||||
|
Color::Black
|
||||||
|
})
|
||||||
|
.bg(mode_accent(mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_cursor(frame: &mut Frame, editor: &Editor, position: Option<Position>) {
|
||||||
|
use crate::cursor::CursorShape;
|
||||||
|
let Some(position) = position.filter(|&position| frame.area().contains(position)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let shape = editor.cursor_config.shape(editor.mode());
|
||||||
|
let colored = editor.cursor_config.mode_colors;
|
||||||
|
let color = if colored {
|
||||||
|
mode_accent(editor.mode())
|
||||||
|
} else {
|
||||||
|
Color::Reset
|
||||||
|
};
|
||||||
|
let visible = Modifier::HIDDEN | Modifier::DIM | Modifier::REVERSED;
|
||||||
|
let style = match shape {
|
||||||
|
CursorShape::Block if colored => mode_style(editor.mode()).remove_modifier(visible),
|
||||||
|
CursorShape::Block => Style::default()
|
||||||
|
.remove_modifier(Modifier::HIDDEN | Modifier::DIM)
|
||||||
|
.add_modifier(Modifier::REVERSED),
|
||||||
|
CursorShape::Underline => Style::default()
|
||||||
|
.fg(color)
|
||||||
|
.underline_color(color)
|
||||||
|
.remove_modifier(visible)
|
||||||
|
.add_modifier(Modifier::UNDERLINED),
|
||||||
|
CursorShape::Bar => Style::default().fg(color).remove_modifier(visible),
|
||||||
|
};
|
||||||
|
let buffer = frame.buffer_mut();
|
||||||
|
let width = buffer[position]
|
||||||
|
.symbol()
|
||||||
|
.width()
|
||||||
|
.max(1)
|
||||||
|
.min(usize::from(buffer.area.right() - position.x));
|
||||||
|
if shape == CursorShape::Bar {
|
||||||
|
// A cell-rendered bar overlays the hovered glyph; source text stays intact.
|
||||||
|
buffer[position].set_symbol("▏");
|
||||||
|
}
|
||||||
|
for offset in 0..width {
|
||||||
|
buffer[(position.x + offset as u16, position.y)].set_style(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep Ratatui's logical cursor position (including IME placement), but never
|
||||||
|
// show a hardware cursor on top of the cell-rendered caret.
|
||||||
|
struct SoftwareCursorBackend<B>(B);
|
||||||
|
|
||||||
|
impl<B: Backend> Backend for SoftwareCursorBackend<B> {
|
||||||
|
type Error = B::Error;
|
||||||
|
|
||||||
|
fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
|
||||||
|
where
|
||||||
|
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||||
|
{
|
||||||
|
self.0.draw(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_lines(&mut self, count: u16) -> Result<(), Self::Error> {
|
||||||
|
self.0.append_lines(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hide_cursor(&mut self) -> Result<(), Self::Error> {
|
||||||
|
self.0.hide_cursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_cursor(&mut self) -> Result<(), Self::Error> {
|
||||||
|
self.0.hide_cursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cursor_position(&mut self) -> Result<Position, Self::Error> {
|
||||||
|
self.0.get_cursor_position()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), Self::Error> {
|
||||||
|
self.0.set_cursor_position(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) -> Result<(), Self::Error> {
|
||||||
|
self.0.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error> {
|
||||||
|
self.0.clear_region(clear_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Result<Size, Self::Error> {
|
||||||
|
self.0.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_size(&mut self) -> Result<WindowSize, Self::Error> {
|
||||||
|
self.0.window_size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> Result<(), Self::Error> {
|
||||||
|
self.0.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_cursor(frame: &mut Frame, panels: &mut WheelPanels, position: Position) {
|
||||||
|
panels.cursor = Some(position);
|
||||||
|
frame.set_cursor_position(position);
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_frame<W: io::Write>(
|
fn draw_frame<W: io::Write>(
|
||||||
terminal: &mut Terminal<CrosstermBackend<Vec<u8>>>,
|
terminal: &mut Terminal<SoftwareCursorBackend<CrosstermBackend<Vec<u8>>>>,
|
||||||
output: &mut W,
|
output: &mut W,
|
||||||
clear_screen: bool,
|
clear_screen: bool,
|
||||||
render: impl FnOnce(&mut Frame),
|
render: impl FnOnce(&mut Frame),
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
// Do not suspend the terminal's presentation while computing the next frame.
|
// Do not suspend the terminal's presentation while computing the next frame.
|
||||||
terminal.backend_mut().writer_mut().clear();
|
terminal.backend_mut().0.writer_mut().clear();
|
||||||
crossterm::queue!(terminal.backend_mut(), BeginSynchronizedUpdate)?;
|
crossterm::queue!(terminal.backend_mut().0, BeginSynchronizedUpdate)?;
|
||||||
// Terminals without synchronized updates still hide the painting cursor.
|
// Terminals without synchronized updates still hide the painting cursor.
|
||||||
let result = terminal.hide_cursor().and_then(|()| {
|
let result = terminal.hide_cursor().and_then(|()| {
|
||||||
if clear_screen {
|
if clear_screen {
|
||||||
// Fresh Ratatui buffers are already empty; avoid its blocking cursor query.
|
// Fresh Ratatui buffers are already empty; avoid its blocking cursor query.
|
||||||
crossterm::queue!(
|
crossterm::queue!(
|
||||||
terminal.backend_mut(),
|
terminal.backend_mut().0,
|
||||||
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
|
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
terminal.draw(render).map(|_| ())
|
terminal.draw(render).map(|_| ())
|
||||||
});
|
});
|
||||||
let finish = execute!(terminal.backend_mut(), EndSynchronizedUpdate);
|
let finish = execute!(terminal.backend_mut().0, EndSynchronizedUpdate);
|
||||||
result.and(finish)?;
|
result.and(finish)?;
|
||||||
output.write_all(terminal.backend().writer())?;
|
output.write_all(terminal.backend().0.writer())?;
|
||||||
output.flush()
|
output.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1320,7 +1442,7 @@ fn draw_frame<W: io::Write>(
|
|||||||
/// inside the first synchronized frame rather than exposing a blank screen.
|
/// inside the first synchronized frame rather than exposing a blank screen.
|
||||||
pub fn run(session: &mut EditorSession, mut clear_screen: bool) -> io::Result<()> {
|
pub fn run(session: &mut EditorSession, mut clear_screen: bool) -> io::Result<()> {
|
||||||
let mut output: Stdout = io::stdout();
|
let mut output: Stdout = io::stdout();
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(Vec::new()))?;
|
let mut terminal = Terminal::new(SoftwareCursorBackend(CrosstermBackend::new(Vec::new())))?;
|
||||||
let mut viewports = BufferViewports::default();
|
let mut viewports = BufferViewports::default();
|
||||||
let mut colors = TerminalColors::default();
|
let mut colors = TerminalColors::default();
|
||||||
let mut query_theme = true;
|
let mut query_theme = true;
|
||||||
@@ -1582,6 +1704,7 @@ struct ChromeItem<'a> {
|
|||||||
config: &'a SegmentConfig,
|
config: &'a SegmentConfig,
|
||||||
tab: Option<usize>,
|
tab: Option<usize>,
|
||||||
accent: bool,
|
accent: bool,
|
||||||
|
mode_style: Style,
|
||||||
bold: bool,
|
bold: bool,
|
||||||
first: bool,
|
first: bool,
|
||||||
mandatory: bool,
|
mandatory: bool,
|
||||||
@@ -1706,6 +1829,7 @@ fn chrome_items<'a>(
|
|||||||
config,
|
config,
|
||||||
tab: Some(index),
|
tab: Some(index),
|
||||||
accent: active,
|
accent: active,
|
||||||
|
mode_style: mode_style(editor.mode()),
|
||||||
bold: active,
|
bold: active,
|
||||||
first: index == 0,
|
first: index == 0,
|
||||||
mandatory: active,
|
mandatory: active,
|
||||||
@@ -1831,6 +1955,7 @@ fn chrome_items<'a>(
|
|||||||
text,
|
text,
|
||||||
config,
|
config,
|
||||||
tab: None,
|
tab: None,
|
||||||
|
mode_style: mode_style(editor.mode()),
|
||||||
accent: !matches!(
|
accent: !matches!(
|
||||||
name.as_str(),
|
name.as_str(),
|
||||||
"path"
|
"path"
|
||||||
@@ -1949,7 +2074,7 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec<ChromeItem<'_>>; 3], area: Re
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let available = width - x;
|
let available = width - x;
|
||||||
let highlight = Style::default().fg(Color::Black).bg(Color::Cyan);
|
let highlight = item.mode_style;
|
||||||
let highlight = if item.bold {
|
let highlight = if item.bold {
|
||||||
highlight.add_modifier(Modifier::BOLD)
|
highlight.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
@@ -1973,7 +2098,7 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec<ChromeItem<'_>>; 3], area: Re
|
|||||||
let leading_margin = &PADDING[..item.config.margin[0]];
|
let leading_margin = &PADDING[..item.config.margin[0]];
|
||||||
let trailing_margin = &PADDING[..item.config.margin[1]];
|
let trailing_margin = &PADDING[..item.config.margin[1]];
|
||||||
let edges = item.tab.is_none() || item.accent;
|
let edges = item.tab.is_none() || item.accent;
|
||||||
let edge = Style::default().fg(Color::Cyan);
|
let edge = Style::default().fg(item.mode_style.bg.unwrap_or(Color::Reset));
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(leading_margin, Style::reset()),
|
Span::styled(leading_margin, Style::reset()),
|
||||||
Span::styled(if edges { item.leading_edge() } else { "" }, edge),
|
Span::styled(if edges { item.leading_edge() } else { "" }, edge),
|
||||||
@@ -2787,7 +2912,7 @@ fn draw_document(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_floating_input(frame: &mut Frame, editor: &Editor, area: Rect) -> Option<Rect> {
|
fn draw_floating_input(frame: &mut Frame, editor: &Editor, area: Rect) -> Option<(Rect, Position)> {
|
||||||
if area.width == 0 || area.height == 0 {
|
if area.width == 0 || area.height == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -2848,7 +2973,10 @@ fn draw_floating_input(frame: &mut Frame, editor: &Editor, area: Rect) -> Option
|
|||||||
content,
|
content,
|
||||||
);
|
);
|
||||||
frame.set_cursor_position((content.x + (column - offset) as u16, content.y));
|
frame.set_cursor_position((content.x + (column - offset) as u16, content.y));
|
||||||
Some(panel)
|
Some((
|
||||||
|
panel,
|
||||||
|
Position::new(content.x + (column - offset) as u16, content.y),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_notification(frame: &mut Frame, editor: &Editor, area: Rect) -> Option<Rect> {
|
fn draw_notification(frame: &mut Frame, editor: &Editor, area: Rect) -> Option<Rect> {
|
||||||
@@ -2932,7 +3060,10 @@ fn draw_overlays(
|
|||||||
.is_some_and(|picker| picker.presentation == FilePresentation::Floating);
|
.is_some_and(|picker| picker.presentation == FilePresentation::Floating);
|
||||||
if !covered {
|
if !covered {
|
||||||
if matches!(editor.mode(), Mode::Command | Mode::Search) {
|
if matches!(editor.mode(), Mode::Command | Mode::Search) {
|
||||||
panels.input = draw_floating_input(frame, editor, area);
|
if let Some((panel, cursor)) = draw_floating_input(frame, editor, area) {
|
||||||
|
panels.input = Some(panel);
|
||||||
|
panels.cursor = Some(cursor);
|
||||||
|
}
|
||||||
} else if editor.diff().is_none_or(|diff| diff.phase == Phase::View)
|
} else if editor.diff().is_none_or(|diff| diff.phase == Phase::View)
|
||||||
&& !editor.file_picker().is_some_and(|picker| picker.focused)
|
&& !editor.file_picker().is_some_and(|picker| picker.focused)
|
||||||
{
|
{
|
||||||
@@ -2947,7 +3078,7 @@ fn draw_overlays(
|
|||||||
.notification
|
.notification
|
||||||
.is_some_and(|panel| panel.contains(position))
|
.is_some_and(|panel| panel.contains(position))
|
||||||
{
|
{
|
||||||
frame.set_cursor_position(position);
|
set_cursor(frame, panels, position);
|
||||||
}
|
}
|
||||||
if let Some(completion) = editor.completion()
|
if let Some(completion) = editor.completion()
|
||||||
&& let Some((_, caret)) = caret
|
&& let Some((_, caret)) = caret
|
||||||
@@ -2986,10 +3117,11 @@ fn draw_overlays(
|
|||||||
draw_hover(frame, hover, area, panels);
|
draw_hover(frame, hover, area, panels);
|
||||||
}
|
}
|
||||||
if let Some(diagnostic) = editor.startup_diagnostic() {
|
if let Some(diagnostic) = editor.startup_diagnostic() {
|
||||||
draw_startup_diagnostic(frame, diagnostic, area);
|
panels.cursor = draw_diagnostic_panel(frame, diagnostic, area, "Configuration error", None);
|
||||||
}
|
}
|
||||||
if let Some(output) = editor.command_output() {
|
if let Some(output) = editor.command_output() {
|
||||||
draw_diagnostic_panel(frame, &output.content, area, &output.title, Some(output));
|
panels.cursor =
|
||||||
|
draw_diagnostic_panel(frame, &output.content, area, &output.title, Some(output));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3312,7 +3444,11 @@ fn paint_editor_line(
|
|||||||
left,
|
left,
|
||||||
column,
|
column,
|
||||||
column.saturating_add(1),
|
column.saturating_add(1),
|
||||||
Style::default().fg(Color::Black).bg(Color::Magenta),
|
if editor.cursor_config.mode_colors {
|
||||||
|
mode_style(editor.mode())
|
||||||
|
} else {
|
||||||
|
Style::default().fg(Color::Black).bg(Color::Magenta)
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3648,7 +3784,11 @@ fn draw_diff(
|
|||||||
&& query_area.height > 0
|
&& query_area.height > 0
|
||||||
&& editor.is_none_or(|editor| editor.render_pane_focused())
|
&& editor.is_none_or(|editor| editor.render_pane_focused())
|
||||||
{
|
{
|
||||||
frame.set_cursor_position((query_area.x + (query_width - offset) as u16, query_area.y));
|
set_cursor(
|
||||||
|
frame,
|
||||||
|
panels,
|
||||||
|
Position::new(query_area.x + (query_width - offset) as u16, query_area.y),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(notice) = notice {
|
if let Some(notice) = notice {
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
@@ -4897,17 +5037,13 @@ fn floating_panel(area: Rect) -> Rect {
|
|||||||
panel
|
panel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_startup_diagnostic(frame: &mut Frame, diagnostic: &StartupDiagnostic, area: Rect) {
|
|
||||||
draw_diagnostic_panel(frame, diagnostic, area, "Configuration error", None);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_diagnostic_panel(
|
fn draw_diagnostic_panel(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
diagnostic: &StartupDiagnostic,
|
diagnostic: &StartupDiagnostic,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
title: &str,
|
title: &str,
|
||||||
output: Option<&crate::editor::ShellOutput>,
|
output: Option<&crate::editor::ShellOutput>,
|
||||||
) {
|
) -> Option<Position> {
|
||||||
let startup = output.is_none();
|
let startup = output.is_none();
|
||||||
let panel = floating_panel(area);
|
let panel = floating_panel(area);
|
||||||
frame.render_widget(Clear, panel);
|
frame.render_widget(Clear, panel);
|
||||||
@@ -4988,7 +5124,11 @@ fn draw_diagnostic_panel(
|
|||||||
help_area,
|
help_area,
|
||||||
);
|
);
|
||||||
if startup && text_area.width > 0 && text_area.height > 0 {
|
if startup && text_area.width > 0 && text_area.height > 0 {
|
||||||
frame.set_cursor_position((text_area.x, text_area.y));
|
let position = Position::new(text_area.x, text_area.y);
|
||||||
|
frame.set_cursor_position(position);
|
||||||
|
Some(position)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5707,6 +5847,7 @@ impl Default for DiffColors {
|
|||||||
struct WheelPanels {
|
struct WheelPanels {
|
||||||
workspace_managed: bool,
|
workspace_managed: bool,
|
||||||
caret: Option<(Rect, Position)>,
|
caret: Option<(Rect, Position)>,
|
||||||
|
cursor: Option<Position>,
|
||||||
input: Option<Rect>,
|
input: Option<Rect>,
|
||||||
notification: Option<Rect>,
|
notification: Option<Rect>,
|
||||||
completion: Option<Rect>,
|
completion: Option<Rect>,
|
||||||
@@ -5826,6 +5967,7 @@ impl WheelPanels {
|
|||||||
}
|
}
|
||||||
self.hover = None;
|
self.hover = None;
|
||||||
self.caret = None;
|
self.caret = None;
|
||||||
|
self.cursor = None;
|
||||||
self.input = None;
|
self.input = None;
|
||||||
self.notification = None;
|
self.notification = None;
|
||||||
self.completion = None;
|
self.completion = None;
|
||||||
@@ -6361,7 +6503,11 @@ fn draw_file_list(
|
|||||||
&& (!browsing || picker.entries().len() == 0 || results_area.height == 0)
|
&& (!browsing || picker.entries().len() == 0 || results_area.height == 0)
|
||||||
{
|
{
|
||||||
let cursor_x = if browsing { 0 } else { query_width - offset };
|
let cursor_x = if browsing { 0 } else { query_width - offset };
|
||||||
frame.set_cursor_position((query_area.x + cursor_x as u16, query_area.y));
|
set_cursor(
|
||||||
|
frame,
|
||||||
|
panels,
|
||||||
|
Position::new(query_area.x + cursor_x as u16, query_area.y),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(root) = root {
|
if let Some(root) = root {
|
||||||
frame.render_widget(root, root_area);
|
frame.render_widget(root, root_area);
|
||||||
@@ -6492,10 +6638,14 @@ fn draw_file_list(
|
|||||||
&& selected_index >= first
|
&& selected_index >= first
|
||||||
&& selected_index - first < count
|
&& selected_index - first < count
|
||||||
{
|
{
|
||||||
frame.set_cursor_position((
|
set_cursor(
|
||||||
results_area.x,
|
frame,
|
||||||
results_area.y + (selected_index - first) as u16,
|
panels,
|
||||||
));
|
Position::new(
|
||||||
|
results_area.x,
|
||||||
|
results_area.y + (selected_index - first) as u16,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !show_help {
|
if !show_help {
|
||||||
@@ -6591,7 +6741,11 @@ fn draw_palette(
|
|||||||
.style(Style::default().fg(Color::Yellow)),
|
.style(Style::default().fg(Color::Yellow)),
|
||||||
query_area,
|
query_area,
|
||||||
);
|
);
|
||||||
frame.set_cursor_position((query_area.x + (query_width - offset) as u16, query_area.y));
|
set_cursor(
|
||||||
|
frame,
|
||||||
|
panels,
|
||||||
|
Position::new(query_area.x + (query_width - offset) as u16, query_area.y),
|
||||||
|
);
|
||||||
if palette.entries.is_empty() {
|
if palette.entries.is_empty() {
|
||||||
panels.picker_scroll.palette = None;
|
panels.picker_scroll.palette = None;
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
|
|||||||
@@ -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]
|
#[test]
|
||||||
fn git_blame_boolean_config_typed_toggle_palette_and_wire_validation() {
|
fn git_blame_boolean_config_typed_toggle_palette_and_wire_validation() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
|
|||||||
+268
-59
@@ -323,7 +323,11 @@ fn historical_buffer_markers_retain_real_final_newlines_and_scope() {
|
|||||||
for (x, y, symbol) in [(3, 0, "↵"), (0, 1, "·"), (1, 1, "·"), (7, 1, "↵")] {
|
for (x, y, symbol) in [(3, 0, "↵"), (0, 1, "·"), (1, 1, "·"), (7, 1, "↵")] {
|
||||||
let cell = &buffer[(area.x + x, area.y + y)];
|
let cell = &buffer[(area.x + x, area.y + y)];
|
||||||
assert_eq!(cell.symbol(), symbol);
|
assert_eq!(cell.symbol(), symbol);
|
||||||
assert!(cell.modifier.contains(Modifier::DIM));
|
assert_eq!(
|
||||||
|
cell.modifier.contains(Modifier::DIM),
|
||||||
|
views.panels.cursor != Some(Position::new(area.x + x, area.y + y)),
|
||||||
|
"only the primary cursor removes marker dim styling",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
assert_eq!(buffer[(area.x + 6, area.y + 1)].symbol(), " ");
|
assert_eq!(buffer[(area.x + 6, area.y + 1)].symbol(), " ");
|
||||||
assert_eq!(buffer[(area.x + 6, area.y + 1)].bg, Color::Red);
|
assert_eq!(buffer[(area.x + 6, area.y + 1)].bg, Color::Red);
|
||||||
@@ -847,6 +851,7 @@ fn whitespace_characters_preserve_source_geometry_and_overlay_styles() {
|
|||||||
session.handle(Input::End);
|
session.handle(Input::End);
|
||||||
session.handle(Input::Char('v'));
|
session.handle(Input::Char('v'));
|
||||||
session.handle(Input::Left);
|
session.handle(Input::Left);
|
||||||
|
session.handle(Input::Left);
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| views.draw(frame, session.editor_mut()))
|
.draw(|frame| views.draw(frame, session.editor_mut()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -905,6 +910,7 @@ fn trailing_whitespace_is_opt_in_and_preserves_wrapping_and_selection() {
|
|||||||
session.handle(Input::End);
|
session.handle(Input::End);
|
||||||
session.handle(Input::Char('v'));
|
session.handle(Input::Char('v'));
|
||||||
session.handle(Input::Left);
|
session.handle(Input::Left);
|
||||||
|
session.handle(Input::Left);
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| views.draw(frame, session.editor_mut()))
|
.draw(|frame| views.draw(frame, session.editor_mut()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1290,7 +1296,7 @@ fn resolver_renders_aligned_native_draft_alternative_and_separate_history() {
|
|||||||
);
|
);
|
||||||
assert_eq!(buffer[(x, content.y + 1)].symbol(), "v");
|
assert_eq!(buffer[(x, content.y + 1)].symbol(), "v");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer[(x, content.y + 1)].bg,
|
buffer[(x + 1, content.y + 1)].bg,
|
||||||
views.panels.diff_colors.row[1]
|
views.panels.diff_colors.row[1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1464,7 +1470,7 @@ fn resolver_renders_aligned_native_draft_alternative_and_separate_history() {
|
|||||||
let searched_caret = render(&mut terminal, &mut views, &mut session);
|
let searched_caret = render(&mut terminal, &mut views, &mut session);
|
||||||
assert!(screen_text(&terminal).contains("context 30"));
|
assert!(screen_text(&terminal).contains("context 30"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[(searched_caret.x, searched_caret.y)].bg,
|
terminal.backend().buffer()[(searched_caret.x + 1, searched_caret.y)].bg,
|
||||||
Color::Yellow,
|
Color::Yellow,
|
||||||
"source {:?} ({:?}), terminal {searched_caret:?}, caret {:?}, viewport {:?}, focus {:?}",
|
"source {:?} ({:?}), terminal {searched_caret:?}, caret {:?}, viewport {:?}, focus {:?}",
|
||||||
session.editor().cursor(),
|
session.editor().cursor(),
|
||||||
@@ -5383,9 +5389,10 @@ fn floating_fields_use_border_titles_and_inner_horizontal_padding() {
|
|||||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
||||||
let workspace = Rect::new(3, 2, 74, 8);
|
let workspace = Rect::new(3, 2, 74, 8);
|
||||||
let mut panel = Rect::default();
|
let mut panel = Rect::default();
|
||||||
|
let mut cursor = Position::default();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
panel = draw_floating_input(frame, &editor, workspace).unwrap();
|
(panel, cursor) = draw_floating_input(frame, &editor, workspace).unwrap();
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(panel.x, workspace.x);
|
assert_eq!(panel.x, workspace.x);
|
||||||
@@ -5397,10 +5404,7 @@ fn floating_fields_use_border_titles_and_inner_horizontal_padding() {
|
|||||||
assert_eq!(buffer[(panel.x + 1, panel.y + 1)].symbol(), " ");
|
assert_eq!(buffer[(panel.x + 1, panel.y + 1)].symbol(), " ");
|
||||||
assert_eq!(buffer[(panel.x + 2, panel.y + 1)].symbol(), "n");
|
assert_eq!(buffer[(panel.x + 2, panel.y + 1)].symbol(), "n");
|
||||||
assert_eq!(buffer[(panel.right() - 2, panel.y + 1)].symbol(), " ");
|
assert_eq!(buffer[(panel.right() - 2, panel.y + 1)].symbol(), " ");
|
||||||
assert_eq!(
|
assert_eq!(cursor, Position::new(panel.x + 8, panel.y + 1));
|
||||||
terminal.get_cursor_position().unwrap(),
|
|
||||||
Position::new(panel.x + 8, panel.y + 1)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
pane_screen(
|
pane_screen(
|
||||||
&terminal,
|
&terminal,
|
||||||
@@ -8558,7 +8562,7 @@ fn native_visual_selection_overrides_selected_review_range_background() {
|
|||||||
let mut views = BufferViewports::default();
|
let mut views = BufferViewports::default();
|
||||||
let source = render_native_diff(&mut terminal, &mut views, &mut editor);
|
let source = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[(source.x, source.y)].bg,
|
terminal.backend().buffer()[(source.x + 1, source.y)].bg,
|
||||||
Color::Magenta
|
Color::Magenta
|
||||||
);
|
);
|
||||||
editor.handle(Input::Char('v'));
|
editor.handle(Input::Char('v'));
|
||||||
@@ -9064,10 +9068,6 @@ fn wrapped_diff_aligns_continuations_and_preserves_native_selection_and_caret()
|
|||||||
terminal.backend().buffer()[(caret.x, caret.y)].symbol(),
|
terminal.backend().buffer()[(caret.x, caret.y)].symbol(),
|
||||||
"D"
|
"D"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
terminal.backend().buffer()[(caret.x, caret.y)].fg,
|
|
||||||
Color::DarkGray
|
|
||||||
);
|
|
||||||
editor.handle(Input::Char('v'));
|
editor.handle(Input::Char('v'));
|
||||||
editor.handle(Input::Home);
|
editor.handle(Input::Home);
|
||||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
@@ -9123,10 +9123,6 @@ fn readonly_diff_panes_render_native_caret_visual_search_and_paired_focus() {
|
|||||||
terminal.backend().buffer()[(caret.x, caret.y)].symbol(),
|
terminal.backend().buffer()[(caret.x, caret.y)].symbol(),
|
||||||
"f"
|
"f"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
terminal.backend().buffer()[(caret.x, caret.y)].fg,
|
|
||||||
Color::Magenta
|
|
||||||
);
|
|
||||||
editor.handle(Input::Char('v'));
|
editor.handle(Input::Char('v'));
|
||||||
editor.handle(Input::Right);
|
editor.handle(Input::Right);
|
||||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
@@ -9140,7 +9136,7 @@ fn readonly_diff_panes_render_native_caret_visual_search_and_paired_focus() {
|
|||||||
editor.handle(Input::Char('V'));
|
editor.handle(Input::Char('V'));
|
||||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[(caret.x, caret.y + 1)].bg,
|
terminal.backend().buffer()[(caret.x + 1, caret.y + 1)].bg,
|
||||||
Color::Blue
|
Color::Blue
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -9157,11 +9153,11 @@ fn readonly_diff_panes_render_native_caret_visual_search_and_paired_focus() {
|
|||||||
let searched = render_native_diff(&mut terminal, &mut views, &mut editor);
|
let searched = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
assert!(before.contains(searched));
|
assert!(before.contains(searched));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[(searched.x, searched.y)].bg,
|
terminal.backend().buffer()[(searched.x + 1, searched.y)].bg,
|
||||||
Color::Yellow
|
Color::Yellow
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[(searched.x, searched.y)].fg,
|
terminal.backend().buffer()[(searched.x + 1, searched.y)].fg,
|
||||||
Color::Black
|
Color::Black
|
||||||
);
|
);
|
||||||
let before_row = editor
|
let before_row = editor
|
||||||
@@ -9926,7 +9922,7 @@ fn snippet_highlight_tracks_argument_graphemes_without_filling_the_row() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let buffer = terminal.backend().buffer();
|
let buffer = terminal.backend().buffer();
|
||||||
// A wide glyph's leading cell owns its terminal style.
|
// A wide glyph's leading cell owns its terminal style.
|
||||||
for x in (0..28).filter(|x| *x != 8) {
|
for x in (0..28).filter(|x| *x != 8 && *x != caret.x) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer[(x, 0)].bg == Color::Blue,
|
buffer[(x, 0)].bg == Color::Blue,
|
||||||
selected.contains(&x),
|
selected.contains(&x),
|
||||||
@@ -9935,6 +9931,11 @@ fn snippet_highlight_tracks_argument_graphemes_without_filling_the_row() {
|
|||||||
screen_text(&terminal),
|
screen_text(&terminal),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
assert_eq!(
|
||||||
|
buffer[caret].bg,
|
||||||
|
Color::Green,
|
||||||
|
"cursor overrides the snippet selection"
|
||||||
|
);
|
||||||
assert_eq!(terminal.get_cursor_position().unwrap(), caret);
|
assert_eq!(terminal.get_cursor_position().unwrap(), caret);
|
||||||
}
|
}
|
||||||
editor.handle(Input::Escape);
|
editor.handle(Input::Escape);
|
||||||
@@ -10504,7 +10505,7 @@ end
|
|||||||
"T"
|
"T"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[(cursor.x, cursor.y)].fg,
|
terminal.backend().buffer()[(cursor.x - 1, cursor.y)].fg,
|
||||||
Color::DarkGray
|
Color::DarkGray
|
||||||
);
|
);
|
||||||
assert_eq!(fs::read_to_string(target).unwrap(), target_text);
|
assert_eq!(fs::read_to_string(target).unwrap(), target_text);
|
||||||
@@ -10522,7 +10523,7 @@ fn syntax_tokens_refresh_after_native_edits_and_undo_without_changing_text_on_dr
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Yellow);
|
assert_eq!(terminal.backend().buffer()[(3, 0)].fg, Color::Yellow);
|
||||||
let revision = editor.text_revision();
|
let revision = editor.text_revision();
|
||||||
editor.handle(Input::Right);
|
editor.handle(Input::Right);
|
||||||
terminal
|
terminal
|
||||||
@@ -10571,7 +10572,7 @@ fn syntax_tokens_refresh_after_native_edits_and_undo_without_changing_text_on_dr
|
|||||||
("42", Color::Yellow),
|
("42", Color::Yellow),
|
||||||
("\"ok\"", Color::Green),
|
("\"ok\"", Color::Green),
|
||||||
] {
|
] {
|
||||||
let x = 2 + source.find(token).unwrap() as u16;
|
let x = 2 + (source.find(token).unwrap() + token.len() - 1) as u16;
|
||||||
assert_eq!(buffer[(x, 0)].fg, color, "{token}");
|
assert_eq!(buffer[(x, 0)].fg, color, "{token}");
|
||||||
assert_eq!(buffer[(x, 0)].bg, Color::Reset);
|
assert_eq!(buffer[(x, 0)].bg, Color::Reset);
|
||||||
}
|
}
|
||||||
@@ -10611,7 +10612,7 @@ fn syntax_continuations_above_viewport_refresh_when_the_opening_line_changes() {
|
|||||||
"v"
|
"v"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[(caret.x, caret.y)].fg,
|
terminal.backend().buffer()[(caret.x + 1, caret.y)].fg,
|
||||||
color,
|
color,
|
||||||
"{name}"
|
"{name}"
|
||||||
);
|
);
|
||||||
@@ -10626,7 +10627,7 @@ fn syntax_continuations_above_viewport_refresh_when_the_opening_line_changes() {
|
|||||||
"v"
|
"v"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[(caret.x, caret.y)].fg,
|
terminal.backend().buffer()[(caret.x + 1, caret.y)].fg,
|
||||||
Color::Reset,
|
Color::Reset,
|
||||||
"{name}"
|
"{name}"
|
||||||
);
|
);
|
||||||
@@ -10635,7 +10636,7 @@ fn syntax_continuations_above_viewport_refresh_when_the_opening_line_changes() {
|
|||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[(caret.x, caret.y)].fg,
|
terminal.backend().buffer()[(caret.x + 1, caret.y)].fg,
|
||||||
color,
|
color,
|
||||||
"{name}"
|
"{name}"
|
||||||
);
|
);
|
||||||
@@ -10895,7 +10896,7 @@ fn search_visual_selection_and_secondary_carets_override_semantic_foreground() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let found = editor.search_match().unwrap();
|
let found = editor.search_match().unwrap();
|
||||||
let y = found.start.row as u16;
|
let y = found.start.row as u16;
|
||||||
for x in 4..8 {
|
for x in 5..8 {
|
||||||
assert_eq!(terminal.backend().buffer()[(x, y)].fg, Color::Black);
|
assert_eq!(terminal.backend().buffer()[(x, y)].fg, Color::Black);
|
||||||
assert_eq!(terminal.backend().buffer()[(x, y)].bg, Color::Yellow);
|
assert_eq!(terminal.backend().buffer()[(x, y)].bg, Color::Yellow);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -10908,6 +10909,7 @@ fn search_visual_selection_and_secondary_carets_override_semantic_foreground() {
|
|||||||
editor.handle(Input::Char('e'));
|
editor.handle(Input::Char('e'));
|
||||||
editor.handle(Input::Ctrl('a'));
|
editor.handle(Input::Ctrl('a'));
|
||||||
assert_eq!(editor.selection_count(), 3);
|
assert_eq!(editor.selection_count(), 3);
|
||||||
|
editor.cursor_config.mode_colors = false;
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -10955,7 +10957,7 @@ fn syntax_redetects_saved_paths_without_text_changes_and_plain_text_keeps_defaul
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Magenta);
|
assert_eq!(terminal.backend().buffer()[(3, 0)].fg, Color::Magenta);
|
||||||
let revision = editor.text_revision();
|
let revision = editor.text_revision();
|
||||||
for name in ["source.vampunknown", "source.txt", "again.rs"] {
|
for name in ["source.vampunknown", "source.txt", "again.rs"] {
|
||||||
editor.execute(&format!("w {}", directory.0.join(name).display()));
|
editor.execute(&format!("w {}", directory.0.join(name).display()));
|
||||||
@@ -10964,13 +10966,13 @@ fn syntax_redetects_saved_paths_without_text_changes_and_plain_text_keeps_defaul
|
|||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if name.ends_with(".rs") {
|
if name.ends_with(".rs") {
|
||||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Magenta);
|
assert_eq!(terminal.backend().buffer()[(3, 0)].fg, Color::Magenta);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
viewports.syntax[&editor.buffer_id()].language(),
|
viewports.syntax[&editor.buffer_id()].language(),
|
||||||
Some("Rust")
|
Some("Rust")
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Reset);
|
assert_eq!(terminal.backend().buffer()[(3, 0)].fg, Color::Reset);
|
||||||
assert!(viewports.syntax[&editor.buffer_id()].line(0).is_empty());
|
assert!(viewports.syntax[&editor.buffer_id()].line(0).is_empty());
|
||||||
assert!(viewports.syntax[&editor.buffer_id()].language().is_none());
|
assert!(viewports.syntax[&editor.buffer_id()].language().is_none());
|
||||||
}
|
}
|
||||||
@@ -10989,7 +10991,7 @@ fn syntax_caches_stay_independent_across_buffer_edits_and_retire_closed_ids() {
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Yellow);
|
assert_eq!(terminal.backend().buffer()[(3, 0)].fg, Color::Yellow);
|
||||||
editor.execute(&format!(
|
editor.execute(&format!(
|
||||||
"edit {}",
|
"edit {}",
|
||||||
directory.file("b.py", "# note").display()
|
directory.file("b.py", "# note").display()
|
||||||
@@ -10998,19 +11000,19 @@ fn syntax_caches_stay_independent_across_buffer_edits_and_retire_closed_ids() {
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::DarkGray);
|
assert_eq!(terminal.backend().buffer()[(3, 0)].fg, Color::DarkGray);
|
||||||
editor
|
editor
|
||||||
.apply(vec![crate::editor::Effect::Replace("\"ok\"".into())])
|
.apply(vec![crate::editor::Effect::Replace("\"ok\"".into())])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Green);
|
assert_eq!(terminal.backend().buffer()[(3, 0)].fg, Color::Green);
|
||||||
editor.execute(&format!("buffer {a_id}"));
|
editor.execute(&format!("buffer {a_id}"));
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Yellow);
|
assert_eq!(terminal.backend().buffer()[(3, 0)].fg, Color::Yellow);
|
||||||
assert_eq!(viewports.syntax[&a_id].language(), Some("Rust"));
|
assert_eq!(viewports.syntax[&a_id].language(), Some("Rust"));
|
||||||
assert_eq!(viewports.syntax[&b_id].language(), Some("Python"));
|
assert_eq!(viewports.syntax[&b_id].language(), Some("Python"));
|
||||||
editor.execute(&format!("buffer {b_id}"));
|
editor.execute(&format!("buffer {b_id}"));
|
||||||
@@ -11018,7 +11020,7 @@ fn syntax_caches_stay_independent_across_buffer_edits_and_retire_closed_ids() {
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| viewports.draw(frame, &mut editor))
|
.draw(|frame| viewports.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(terminal.backend().buffer()[(2, 0)].fg, Color::Green);
|
assert_eq!(terminal.backend().buffer()[(3, 0)].fg, Color::Green);
|
||||||
assert!(!viewports.syntax.contains_key(&a_id));
|
assert!(!viewports.syntax.contains_key(&a_id));
|
||||||
assert!(!viewports.offsets.contains_key(&a_id));
|
assert!(!viewports.offsets.contains_key(&a_id));
|
||||||
assert_eq!(viewports.syntax.len(), 1);
|
assert_eq!(viewports.syntax.len(), 1);
|
||||||
@@ -14348,6 +14350,7 @@ fn status_identifies_active_buffer_and_restored_visual_selection_count() {
|
|||||||
let b = directory.file("b", "other");
|
let b = directory.file("b", "other");
|
||||||
let mut editor = Editor::open(Some(a), vec![]).unwrap();
|
let mut editor = Editor::open(Some(a), vec![]).unwrap();
|
||||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||||
|
editor.cursor_config.mode_colors = false;
|
||||||
let a_id = editor.buffer_id();
|
let a_id = editor.buffer_id();
|
||||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(240, 6)).unwrap();
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(240, 6)).unwrap();
|
||||||
let mut viewports = BufferViewports::default();
|
let mut viewports = BufferViewports::default();
|
||||||
@@ -15160,6 +15163,7 @@ fn tabs_and_wide_graphemes_scroll_in_terminal_cells() {
|
|||||||
fn selection_set_renders_all_ranges_and_counts_offscreen_targets() {
|
fn selection_set_renders_all_ranges_and_counts_offscreen_targets() {
|
||||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
editor.set_gutter_style(crate::gutter::GutterStyle::None);
|
||||||
|
editor.cursor_config.mode_colors = false;
|
||||||
editor
|
editor
|
||||||
.apply(vec![crate::editor::Effect::Replace(
|
.apply(vec![crate::editor::Effect::Replace(
|
||||||
"界\n界\n界\n界\n界".into(),
|
"界\n界\n界\n界\n界".into(),
|
||||||
@@ -15214,25 +15218,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 terminal = Terminal::new(ratatui::backend::TestBackend::new(48, 5)).unwrap();
|
||||||
let (mut top, mut left) = (0, 0);
|
let (mut top, mut left) = (0, 0);
|
||||||
let mut syntax = SyntaxCache::new();
|
let mut syntax = SyntaxCache::new();
|
||||||
terminal
|
for (enabled, expected) in [(true, Color::Green), (false, Color::Magenta)] {
|
||||||
.draw(|frame| {
|
editor.cursor_config.mode_colors = enabled;
|
||||||
draw(
|
terminal
|
||||||
frame,
|
.draw(|frame| {
|
||||||
&editor,
|
draw(
|
||||||
&mut top,
|
frame,
|
||||||
&mut left,
|
&editor,
|
||||||
&mut syntax,
|
&mut top,
|
||||||
&mut DocumentViewport::default(),
|
&mut left,
|
||||||
&mut WheelPanels::default(),
|
&mut syntax,
|
||||||
)
|
&mut DocumentViewport::default(),
|
||||||
})
|
&mut WheelPanels::default(),
|
||||||
.unwrap();
|
)
|
||||||
let buffer = terminal.backend().buffer();
|
})
|
||||||
for caret in editor.carets() {
|
.unwrap();
|
||||||
let x = 2 + display_column(&editor.lines()[caret.row], caret.column) as u16;
|
let buffer = terminal.backend().buffer();
|
||||||
assert_eq!(buffer[(x, caret.row as u16)].symbol(), " ");
|
for caret in editor.carets() {
|
||||||
if caret != editor.cursor() {
|
let x = 2 + display_column(&editor.lines()[caret.row], caret.column) as u16;
|
||||||
assert_eq!(buffer[(x, caret.row as u16)].bg, Color::Magenta);
|
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!(
|
assert_eq!(
|
||||||
@@ -15590,6 +15597,121 @@ fn active_search_highlight_uses_display_cells_and_clips_with_horizontal_scroll()
|
|||||||
assert!(content.contains(terminal.get_cursor_position().unwrap()));
|
assert!(content.contains(terminal.get_cursor_position().unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn software_cursor_tracks_mode_palette_and_preserves_document_text() {
|
||||||
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
editor
|
||||||
|
.apply(vec![crate::editor::Effect::Replace("hello\n".into())])
|
||||||
|
.unwrap();
|
||||||
|
let mut terminal = Terminal::new(SoftwareCursorBackend(ratatui::backend::TestBackend::new(
|
||||||
|
60, 12,
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
let mut views = BufferViewports::default();
|
||||||
|
for (inputs, mode, expected) in [
|
||||||
|
(vec![], Mode::Normal, Color::Cyan),
|
||||||
|
(vec![Input::Char('i')], Mode::Insert, Color::Green),
|
||||||
|
(
|
||||||
|
vec![Input::Escape, Input::Char('v')],
|
||||||
|
Mode::Visual,
|
||||||
|
Color::Magenta,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![Input::Escape, Input::Char(':')],
|
||||||
|
Mode::Command,
|
||||||
|
Color::Yellow,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
vec![Input::Escape, Input::Char('/')],
|
||||||
|
Mode::Search,
|
||||||
|
Color::Blue,
|
||||||
|
),
|
||||||
|
(vec![Input::Escape], Mode::Normal, Color::Cyan),
|
||||||
|
] {
|
||||||
|
for input in inputs {
|
||||||
|
editor.handle(input);
|
||||||
|
}
|
||||||
|
assert_eq!(editor.mode(), mode);
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
let position = if matches!(mode, Mode::Command | Mode::Search) {
|
||||||
|
let panel = views.panels.input.unwrap();
|
||||||
|
Position::new(panel.x + 2, panel.y + 1)
|
||||||
|
} else {
|
||||||
|
views.panels.caret.unwrap().1
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
terminal.backend().0.buffer()[position].bg,
|
||||||
|
expected,
|
||||||
|
"{mode}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!terminal.backend().0.cursor_visible(),
|
||||||
|
"hardware cursor must stay hidden"
|
||||||
|
);
|
||||||
|
assert_eq!(editor.lines(), &["hello", ""]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn software_cursor_shapes_preserve_wide_text_and_restore_cells_after_motion() {
|
||||||
|
use crate::cursor::CursorShape;
|
||||||
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
editor
|
||||||
|
.apply(vec![crate::editor::Effect::Replace("界e\u{301}\n".into())])
|
||||||
|
.unwrap();
|
||||||
|
let mut terminal = Terminal::new(SoftwareCursorBackend(ratatui::backend::TestBackend::new(
|
||||||
|
40, 8,
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
let mut views = BufferViewports::default();
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
let position = views.panels.cursor.unwrap();
|
||||||
|
let buffer = terminal.backend().0.buffer();
|
||||||
|
assert_eq!(buffer[position].symbol(), "界");
|
||||||
|
assert_eq!(buffer[position].bg, Color::Cyan);
|
||||||
|
|
||||||
|
editor.cursor_config.normal = CursorShape::Bar;
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(terminal.backend().0.buffer()[position].symbol(), "▏");
|
||||||
|
assert_eq!(terminal.backend().0.buffer()[position].fg, Color::Cyan);
|
||||||
|
editor.handle(Input::Right);
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(terminal.backend().0.buffer()[position].symbol(), "界");
|
||||||
|
let next = views.panels.cursor.unwrap();
|
||||||
|
assert_eq!(next.x, position.x + 2);
|
||||||
|
assert_eq!(terminal.backend().0.buffer()[next].symbol(), "▏");
|
||||||
|
|
||||||
|
editor.cursor_config.normal = CursorShape::Underline;
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
let buffer = terminal.backend().0.buffer();
|
||||||
|
assert_eq!(buffer[next].symbol(), "e\u{301}");
|
||||||
|
assert!(buffer[next].modifier.contains(Modifier::UNDERLINED));
|
||||||
|
assert_eq!(buffer[next].underline_color, Color::Cyan);
|
||||||
|
assert_eq!(buffer[next].fg, Color::Cyan);
|
||||||
|
|
||||||
|
editor.cursor_config.normal = CursorShape::Block;
|
||||||
|
editor.cursor_config.mode_colors = false;
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
let buffer = terminal.backend().0.buffer();
|
||||||
|
assert_eq!(buffer[next].symbol(), "e\u{301}");
|
||||||
|
assert!(buffer[next].modifier.contains(Modifier::REVERSED));
|
||||||
|
assert_eq!(buffer[next].bg, Color::Reset);
|
||||||
|
assert_eq!(editor.lines(), &["界e\u{301}", ""]);
|
||||||
|
assert!(!terminal.backend().0.cursor_visible());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn terminal_can_present_previous_frame_while_next_frame_is_computed() {
|
fn terminal_can_present_previous_frame_while_next_frame_is_computed() {
|
||||||
use std::{cell::Cell, rc::Rc};
|
use std::{cell::Cell, rc::Rc};
|
||||||
@@ -15621,7 +15743,7 @@ fn terminal_can_present_previous_frame_while_next_frame_is_computed() {
|
|||||||
synchronized: Rc::clone(&synchronized),
|
synchronized: Rc::clone(&synchronized),
|
||||||
};
|
};
|
||||||
let mut terminal = Terminal::with_options(
|
let mut terminal = Terminal::with_options(
|
||||||
CrosstermBackend::new(Vec::new()),
|
SoftwareCursorBackend(CrosstermBackend::new(Vec::new())),
|
||||||
ratatui::TerminalOptions {
|
ratatui::TerminalOptions {
|
||||||
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
|
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
|
||||||
},
|
},
|
||||||
@@ -15650,7 +15772,7 @@ fn scrolling_never_paints_with_an_exposed_cursor_even_without_synchronized_updat
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
let mut terminal = Terminal::with_options(
|
let mut terminal = Terminal::with_options(
|
||||||
CrosstermBackend::new(Vec::new()),
|
SoftwareCursorBackend(CrosstermBackend::new(Vec::new())),
|
||||||
ratatui::TerminalOptions {
|
ratatui::TerminalOptions {
|
||||||
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
|
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
|
||||||
},
|
},
|
||||||
@@ -15701,7 +15823,10 @@ fn scrolling_never_paints_with_an_exposed_cursor_even_without_synchronized_updat
|
|||||||
x += 1;
|
x += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert!(visible);
|
assert!(
|
||||||
|
!visible,
|
||||||
|
"hardware cursor must stay hidden behind the software cursor"
|
||||||
|
);
|
||||||
assert!(!synchronized);
|
assert!(!synchronized);
|
||||||
for (row, expected) in [(0, "row 0"), (1, "row 1"), (2, "row 2")] {
|
for (row, expected) in [(0, "row 0"), (1, "row 1"), (2, "row 2")] {
|
||||||
let line = std::str::from_utf8(&screen[row]).unwrap().trim_end();
|
let line = std::str::from_utf8(&screen[row]).unwrap().trim_end();
|
||||||
@@ -15734,7 +15859,7 @@ fn terminal_session_scopes_the_callers_panic_hook() {
|
|||||||
"error" => Err(io::Error::from(io::ErrorKind::BrokenPipe)),
|
"error" => Err(io::Error::from(io::ErrorKind::BrokenPipe)),
|
||||||
"panic" => {
|
"panic" => {
|
||||||
let mut terminal = Terminal::with_options(
|
let mut terminal = Terminal::with_options(
|
||||||
CrosstermBackend::new(Vec::new()),
|
SoftwareCursorBackend(CrosstermBackend::new(Vec::new())),
|
||||||
ratatui::TerminalOptions {
|
ratatui::TerminalOptions {
|
||||||
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
|
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
|
||||||
},
|
},
|
||||||
@@ -15952,6 +16077,8 @@ end
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Document-layer checks exclude the final primary-cursor overlay. Complete-frame
|
||||||
|
// cursor and banner checks render through BufferViewports instead.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn draw(
|
fn draw(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
@@ -16163,3 +16290,85 @@ fn shared_shell_status_is_a_separate_segment_left_of_the_battery() {
|
|||||||
assert!(!top.contains("sle..."));
|
assert!(!top.contains("sle..."));
|
||||||
assert!(top.trim_end().ends_with("bat 27%"), "{top}");
|
assert!(top.trim_end().ends_with("bat 27%"), "{top}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn both_banners_follow_mode_with_neutral_gaps_even_when_cursor_colors_are_disabled() {
|
||||||
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
editor.execute("tab.new");
|
||||||
|
editor.cursor_config.mode_colors = false;
|
||||||
|
configure_chrome(&mut editor, |top, bottom| {
|
||||||
|
top.left = vec!["tabs".into()];
|
||||||
|
top.center = vec!["path".into()];
|
||||||
|
top.right = vec!["badge".into()];
|
||||||
|
for name in ["tabs", "badge", "path"] {
|
||||||
|
top.segments.insert(
|
||||||
|
name.into(),
|
||||||
|
SegmentConfig {
|
||||||
|
title: match name {
|
||||||
|
"tabs" => "{index}".into(),
|
||||||
|
"path" => "N".into(),
|
||||||
|
_ => String::new(),
|
||||||
|
},
|
||||||
|
padding: usize::from(name != "path"),
|
||||||
|
margin: [1; 2],
|
||||||
|
leading_edge: if name == "path" { "" } else { "<" }.into(),
|
||||||
|
trailing_edge: if name == "path" { "" } else { ">" }.into(),
|
||||||
|
first_leading_edge: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*bottom = top.clone();
|
||||||
|
});
|
||||||
|
editor.set_label("badge".into(), "B".into()).unwrap();
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(60, 6)).unwrap();
|
||||||
|
let mut syntax = SyntaxCache::new();
|
||||||
|
for (input, mode, accent, foreground) in [
|
||||||
|
(None, Mode::Normal, Color::Cyan, Color::Black),
|
||||||
|
(Some('i'), Mode::Insert, Color::Green, Color::Black),
|
||||||
|
(Some('v'), Mode::Visual, Color::Magenta, Color::Black),
|
||||||
|
(Some(':'), Mode::Command, Color::Yellow, Color::Black),
|
||||||
|
(Some('/'), Mode::Search, Color::Blue, Color::White),
|
||||||
|
(None, Mode::Normal, Color::Cyan, Color::Black),
|
||||||
|
] {
|
||||||
|
editor.handle(Input::Escape);
|
||||||
|
if let Some(ch) = input {
|
||||||
|
editor.handle(Input::Char(ch));
|
||||||
|
}
|
||||||
|
assert_eq!(editor.mode(), mode);
|
||||||
|
terminal
|
||||||
|
.draw(|frame| {
|
||||||
|
let area = frame.area();
|
||||||
|
assert_eq!(
|
||||||
|
draw_bars(frame, &editor, area, &mut syntax),
|
||||||
|
Rect::new(0, 1, 60, 4)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
for y in [0, 5] {
|
||||||
|
for (x, symbol) in [(9, "2"), (56, "B")] {
|
||||||
|
assert_eq!(buffer[(x, y)].symbol(), symbol, "{mode:?}");
|
||||||
|
}
|
||||||
|
for x in [8, 9, 10, 55, 56, 57] {
|
||||||
|
assert_eq!(buffer[(x, y)].bg, accent, "{mode:?} at {x},{y}");
|
||||||
|
assert_eq!(buffer[(x, y)].fg, foreground, "{mode:?} at {x},{y}");
|
||||||
|
}
|
||||||
|
for (x, symbol) in [(7, "<"), (11, ">"), (54, "<"), (58, ">")] {
|
||||||
|
assert_eq!(buffer[(x, y)].symbol(), symbol);
|
||||||
|
assert_eq!(buffer[(x, y)].fg, accent, "{mode:?} at {x},{y}");
|
||||||
|
assert_eq!(buffer[(x, y)].bg, Color::Reset);
|
||||||
|
}
|
||||||
|
assert_eq!(buffer[(2, y)].symbol(), "1");
|
||||||
|
assert_eq!(buffer[(2, y)].fg, Color::Gray);
|
||||||
|
assert_eq!(buffer[(2, y)].bg, Color::Reset);
|
||||||
|
assert_eq!(buffer[(29, y)].symbol(), "N");
|
||||||
|
assert_eq!(buffer[(29, y)].fg, Color::Reset);
|
||||||
|
assert_eq!(buffer[(29, y)].bg, Color::Reset);
|
||||||
|
for x in [0, 4, 5, 6, 12, 13, 28, 30, 52, 53, 59] {
|
||||||
|
assert_eq!(buffer[(x, y)].symbol(), " ");
|
||||||
|
assert_eq!(buffer[(x, y)].fg, Color::Reset, "{mode:?} at {x},{y}");
|
||||||
|
assert_eq!(buffer[(x, y)].bg, Color::Reset, "{mode:?} at {x},{y}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user