feat: configure interchangeable top and bottom bars
This commit is contained in:
@@ -9,7 +9,7 @@ Contributor-facing module boundaries and implementation contracts. Project termi
|
|||||||
One editor crate; the workspace also includes its patched Crossterm dependency. Terminal-coordinate types remain outside the editing backend:
|
One editor crate; the workspace also includes its patched Crossterm dependency. Terminal-coordinate types remain outside the editing backend:
|
||||||
|
|
||||||
- `editor`: owns shared buffer lifecycle/text/saved state/undo history and independent tab/pane layouts, focused views, modes, selections, searches, explorers, and comparisons; session-wide clipboard policy, independent registers and owned-system fragment metadata, semantic native repeat, action metadata, command-palette and file-picker focus/dispatch, key notation, key-sequence/count prefixes and binding resolution, modal paste policy, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
|
- `editor`: owns shared buffer lifecycle/text/saved state/undo history and independent tab/pane layouts, focused views, modes, selections, searches, explorers, and comparisons; session-wide clipboard policy, independent registers and owned-system fragment metadata, semantic native repeat, action metadata, command-palette and file-picker focus/dispatch, key notation, key-sequence/count prefixes and binding resolution, modal paste policy, saving, command ownership, capabilities, and validated edit effects. Its low-level interface returns an `Invocation` when an external command is selected.
|
||||||
- `chrome`: owns validated tabline/statusline anchor groups, segment title templates and padding/edge options, builtin defaults, and named-value validation. It has no terminal geometry or render callbacks.
|
- `chrome`: owns validated top/bottom bar anchor groups, segment title templates and padding/edge options, builtin defaults, and named-value validation. Both bars accept the same segments and layout options. It has no terminal geometry or render callbacks.
|
||||||
- `files`: owns the gitignore-aware directory/file catalog, authoritative OS paths, compact tree projection, query filtering, browse/search focus, expansion/selection restoration, navigation, and scan/error state shared by floating and docked presentations.
|
- `files`: owns the gitignore-aware directory/file catalog, authoritative OS paths, compact tree projection, query filtering, browse/search focus, expansion/selection restoration, navigation, and scan/error state shared by floating and docked presentations.
|
||||||
Virtual file leaves retain their original catalog input index for caller-owned annotations; scanned entries and directories remain unannotated.
|
Virtual file leaves retain their original catalog input index for caller-owned annotations; scanned entries and directories remain unannotated.
|
||||||
- `diff`: owns searchable Before/After selection, category, focus and explorer visibility, request freshness, read-only comparison DTOs, aligned rows with cached grapheme-level change ranges, and shared vertical/independent horizontal offsets. Changed paths form a virtual `files::FilePicker` catalog, reusing the normal tree projection, filtering, and navigation without scanning disk. It has no subprocess or terminal dependencies.
|
- `diff`: owns searchable Before/After selection, category, focus and explorer visibility, request freshness, read-only comparison DTOs, aligned rows with cached grapheme-level change ranges, and shared vertical/independent horizontal offsets. Changed paths form a virtual `files::FilePicker` catalog, reusing the normal tree projection, filtering, and navigation without scanning disk. It has no subprocess or terminal dependencies.
|
||||||
@@ -39,7 +39,7 @@ Frontends inspect `line_operator_pending()` for the optional Normal `d`/`c` pref
|
|||||||
Editor-owned jump history is a bounded 100-location `VecDeque` of buffer IDs and grapheme cursors plus a current index, with no copied paths/text. Successful high-level navigation records departure/destination once; traversal uses unrecorded buffer activation and position placement, skips closed/clamped-duplicate locations, and truncates forward entries only on a distinct new jump. Session synchronization permanently invalidates pending LSP position requests after their source buffer/revision/caret/mode is left, so returning through history cannot revive an old definition reply.
|
Editor-owned jump history is a bounded 100-location `VecDeque` of buffer IDs and grapheme cursors plus a current index, with no copied paths/text. Successful high-level navigation records departure/destination once; traversal uses unrecorded buffer activation and position placement, skips closed/clamped-duplicate locations, and truncates forward entries only on a distinct new jump. Session synchronization permanently invalidates pending LSP position requests after their source buffer/revision/caret/mode is left, so returning through history cannot revive an old definition reply.
|
||||||
Frontends inspect `buffer_id()`, `buffer_count()`, `has_buffer(id)`, and the ascending-ID `buffers()` iterator of borrowed path/dirty/active metadata. Existing text, cursor, selection, search, and mode methods describe only the active buffer.
|
Frontends inspect `buffer_id()`, `buffer_count()`, `has_buffer(id)`, and the ascending-ID `buffers()` iterator of borrowed path/dirty/active metadata. Existing text, cursor, selection, search, and mode methods describe only the active buffer.
|
||||||
Frontends inspect `tab_id()` for stable identity, `tab_index()` for display order, `tab_count()`, and `tab_buffer(index)` for borrowed title metadata. `pane_layout()` describes only the active tab; globally unique pane IDs keep inactive tabs' viewports alive. Documents remain unique across all tabs, while independent pane/buffer views are parked and restored without copying text.
|
Frontends inspect `tab_id()` for stable identity, `tab_index()` for display order, `tab_count()`, and `tab_buffer(index)` for borrowed title metadata. `pane_layout()` describes only the active tab; globally unique pane IDs keep inactive tabs' viewports alive. Documents remain unique across all tabs, while independent pane/buffer views are parked and restored without copying text.
|
||||||
Frontends borrow `tabline_config()` and `statusline_config()` for validated ordered left/center/right segment groups and effective segment styles, and `label(name)` for session-wide custom values. The TUI owns terminal-cell widths, full-row center anchoring, clipping, and accent styling. Label effects carry only a name and plain text; they are global presentation updates, not buffer/revision-bound results or render callbacks.
|
Frontends borrow `top_bar_config()` and `bottom_bar_config()` for validated ordered left/center/right segment groups and effective segment styles, and `label(name)` for session-wide custom values. The TUI owns terminal-cell widths, full-row center anchoring, clipping, and accent styling. Label effects carry only a name and plain text; they are global presentation updates, not buffer/revision-bound results or render callbacks.
|
||||||
|
|
||||||
Frontends inspect `file_picker()` for query, presentation, view, explorer-local INSERT/filter subfocus, selection, and errors, and iterate borrowed `FileRow` metadata (actual path, display label, directory flag, visible depth, expansion). Dock side is a logical preference. They allocate rectangles, add folder suffixes/colors, and render; hierarchy, filtering, parent/child navigation, focus transitions, and selected-file opening remain backend-owned.
|
Frontends inspect `file_picker()` for query, presentation, view, explorer-local INSERT/filter subfocus, selection, and errors, and iterate borrowed `FileRow` metadata (actual path, display label, directory flag, visible depth, expansion). Dock side is a logical preference. They allocate rectangles, add folder suffixes/colors, and render; hierarchy, filtering, parent/child navigation, focus transitions, and selected-file opening remain backend-owned.
|
||||||
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.
|
||||||
@@ -58,7 +58,7 @@ Redraws stage Ratatui's buffer diff, cursor placement, and DEC synchronized-upda
|
|||||||
|
|
||||||
`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 uses Ratatui constraint-based layout for a flexible buffer viewport, a one-row statusline, and a one-row prompt/message area. Multiple tabs or visible custom top-line segments reserve a tabline row; overflow keeps the active tab title visible. Each chrome row renders configured ordered left/center/right groups, with center anchored to the full row rather than the remaining gap. Padding is inside segment styling, while leading/trailing edge glyphs are separate terminal-cell-width decorations. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. Below three workspace rows the statusline is hidden; at one workspace row the buffer remains visible unless an active command/search prompt takes priority. Zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned.
|
The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, optional one-row top and bottom bars, and a separate prompt/message area. Both bars derive visibility from their configured content before allocating rows: empty content takes no row, and tabs-only content is hidden with a single tab in either position. Both bars render across ordinary, split, diff, and selector views. Each bar renders configured ordered left/center/right groups, with center anchored to the full row rather than the remaining gap. Overflow keeps the active tab title visible wherever tabs appear. Padding is inside segment styling, while leading/trailing edge glyphs are separate terminal-cell-width decorations. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. An active command/search prompt takes priority in minimal height; zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned.
|
||||||
|
|
||||||
Mouse wheel input uses the last rendered document/panel rectangles for hit-testing. Document offsets and explorer scroll state remain frontend-owned; three-row wheel steps never become document cursor motions. Manual document views persist until logical caret-reveal, caret/selection, mode, revision, or buffer changes; offscreen primary and browsing-selection cursors are not placed. Hover wheel input reuses the existing editor-owned popup scrolling.
|
Mouse wheel input uses the last rendered document/panel rectangles for hit-testing. Document offsets and explorer scroll state remain frontend-owned; three-row wheel steps never become document cursor motions. Manual document views persist until logical caret-reveal, caret/selection, mode, revision, or buffer changes; offscreen primary and browsing-selection cursors are not placed. Hover wheel input reuses the existing editor-owned popup scrolling.
|
||||||
|
|
||||||
|
|||||||
+17
-11
@@ -44,13 +44,23 @@ Plugin paths resolve relative to the config file, not the shell's working direct
|
|||||||
|
|
||||||
`config.diff_context_lines = 10` sets how many unchanged aligned lines appear above and below each diff change; it defaults to `10` and accepts nonnegative integers, including `0` for changes only. `config.diff_expand_lines = 15` sets how many additional lines each directional expansion reveals per keypress; it defaults to `15` and accepts positive integers. Both are bounded by the platform's Rust `usize` maximum (`18446744073709551615` on 64-bit systems). Floats, strings, booleans, negative or oversized values, and a zero expansion step reject the whole Ruby configuration.
|
`config.diff_context_lines = 10` sets how many unchanged aligned lines appear above and below each diff change; it defaults to `10` and accepts nonnegative integers, including `0` for changes only. `config.diff_expand_lines = 15` sets how many additional lines each directional expansion reveals per keypress; it defaults to `15` and accepts positive integers. Both are bounded by the platform's Rust `usize` maximum (`18446744073709551615` on 64-bit systems). Floats, strings, booleans, negative or oversized values, and a zero expansion step reject the whole Ruby configuration.
|
||||||
|
|
||||||
### Tabline and statusline segments
|
### Top and bottom bars
|
||||||
|
|
||||||
`config.tabline` and `config.statusline` each accept ordered `left`, `center`, and `right` arrays of segment names. 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 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, 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.
|
||||||
|
|
||||||
|
Use the two setters independently; a combined `config.bar = { ... }` assignment is not supported.
|
||||||
|
|
||||||
|
Defaults are unchanged: the top has `left: [:tabs]`; the bottom has `left: [:mode, :pending, :buffer, :diagnostics, :path, :selections, :count, :language]` and `right: [:position]`; other groups are empty. Set the bars independently, as in this swapped layout with status information above and tabs below:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Vamp.configure do |config|
|
Vamp.configure do |config|
|
||||||
config.tabline = {
|
config.bar.top = {
|
||||||
|
left: [:mode, :path, :language, :diagnostics, :selections, :count, :pending, :buffer],
|
||||||
|
center: [], right: [:position],
|
||||||
|
mode: { padding: 1, leading_edge: "", trailing_edge: "▓▒░" },
|
||||||
|
position: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
||||||
|
}
|
||||||
|
config.bar.bottom = {
|
||||||
left: [:tabs], center: [], right: [:battery],
|
left: [:tabs], center: [], right: [:battery],
|
||||||
tabs: {
|
tabs: {
|
||||||
title: "{index} {name}{modified}", padding: 1,
|
title: "{index} {name}{modified}", padding: 1,
|
||||||
@@ -58,23 +68,19 @@ Vamp.configure do |config|
|
|||||||
},
|
},
|
||||||
battery: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
battery: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
||||||
}
|
}
|
||||||
config.statusline = {
|
|
||||||
left: [:mode, :path, :language, :diagnostics, :selections, :count, :pending, :buffer],
|
|
||||||
center: [], right: [:position],
|
|
||||||
mode: { padding: 1, leading_edge: "", trailing_edge: "▓▒░" },
|
|
||||||
position: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
|
||||||
}
|
|
||||||
config.labels = { battery: "bat 38%" }
|
config.labels = { battery: "bat 38%" }
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
The top line's builtin is `:tabs`; the bottom line's builtins are `:mode`, `:path`, `:position`, `:language`, `:diagnostics`, `:selections`, `:count`, `:pending`, and `:buffer`. They provide the mode, file path/dirty state, caret position, detected language, diagnostics, selection count, numeric prefix, pending key sequence, and buffer identity/count. Using a builtin on the wrong line rejects configuration. Defaults retain the existing information and accent styling: the top has `left: [:tabs]`; the bottom has `left: [:mode, :pending, :buffer, :diagnostics, :path, :selections, :count, :language]` and `right: [:position]`; other groups are empty.
|
Builtins are `:tabs`, `:mode`, `:path`, `:position`, `:language`, `:diagnostics`, `:selections`, `:count`, `:pending`, and `:buffer`, in either bar. They provide tab titles, mode, file path/dirty state, caret position, detected language, diagnostics, selection count, numeric prefix, pending key sequence, and buffer identity/count.
|
||||||
|
|
||||||
|
A bar with no visible content consumes no row. For example, `config.bar.top = { left: [], center: [], right: [] }` removes the top bar. A bar whose only visible content is `:tabs` is hidden with a single tab, whether above or below the workspace. Narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped. The command/search prompt and message area remain separate from both bars.
|
||||||
|
|
||||||
Each segment's `title` is a literal template supporting only `{index}`, `{name}`, `{path}`, and `{modified}`: the one-based tab index, filename, file path, and dirty marker (`" [+]"` when dirty, `""` when clean). Tabs use each tab's buffer metadata; other segments use the active buffer. Tabs default to `"{index} {name}{modified}"`; an empty title on another segment uses its builtin content or named label value. Templates are not Ruby code and never invoke a render callback.
|
Each segment's `title` is a literal template supporting only `{index}`, `{name}`, `{path}`, and `{modified}`: the one-based tab index, filename, file path, and dirty marker (`" [+]"` when dirty, `""` when clean). Tabs use each tab's buffer metadata; other segments use the active buffer. Tabs default to `"{index} {name}{modified}"`; an empty title on another segment uses its builtin content or named label value. Templates are not Ruby code and never invoke a render callback.
|
||||||
|
|
||||||
`padding` adds spaces inside the segment's styling, on both sides of its title, and accepts integers from `0` through `64` (default `1`). `leading_edge` and `trailing_edge` are separate styled glyph strings, not padding; `""` removes an edge. Tabs default to `"░▒▓"` / `"▓▒░"` edges; mode has only the trailing `"▓▒░"` edge, position only the leading `"░▒▓"` edge, and other segments have none. `first_leading_edge` optionally overrides the first occurrence's leading glyphs: for tabs this is the actual first tab, not the first tab remaining after clipping; other segments have one occurrence. Tabs default this override to `""`, suppressing the first tab's left fade. Unicode widths are measured in terminal cells. Edge strings must be single-line and contain no control characters. Unknown style keys, invalid templates, and wrong types reject configuration.
|
`padding` adds spaces inside the segment's styling, on both sides of its title, and accepts integers from `0` through `64` (default `1`). `leading_edge` and `trailing_edge` are separate styled glyph strings, not padding; `""` removes an edge. Tabs default to `"░▒▓"` / `"▓▒░"` edges; mode has only the trailing `"▓▒░"` edge, position only the leading `"░▒▓"` edge, and other segments have none. `first_leading_edge` optionally overrides the first occurrence's leading glyphs: for tabs this is the actual first tab, not the first tab remaining after clipping; other segments have one occurrence. Tabs default this override to `""`, suppressing the first tab's left fade. Unicode widths are measured in terminal cells. Edge strings must be single-line and contain no control characters. Unknown style keys, invalid templates, and wrong types reject configuration.
|
||||||
|
|
||||||
Any other segment name is a custom named value, such as `:battery`. Set initial strings with `config.labels`; an empty value hides the custom segment, including its title override, padding, and edges. Names match `[A-Za-z_][A-Za-z0-9_]*`. Labels are plain text: control characters are replaced rather than interpreted as terminal escapes. The same named value can appear on either line.
|
Any other segment name is a custom named value, such as `:battery`. Set initial strings with `config.labels`; an empty value hides the custom segment, including its title override, padding, and edges. Names match `[A-Za-z_][A-Za-z0-9_]*`. Labels are plain text: control characters are replaced rather than interpreted as terminal escapes. The same named value can appear in either bar.
|
||||||
|
|
||||||
Configuration runs once at startup. For live values, use [`editor.label(name, text)` or asynchronous plugin updates](plugins.md#named-label-updates). The editor does not collect battery data, run periodic shell commands, or accept render callbacks; a plugin supplies values and chooses when to refresh them.
|
Configuration runs once at startup. For live values, use [`editor.label(name, text)` or asynchronous plugin updates](plugins.md#named-label-updates). The editor does not collect battery data, run periodic shell commands, or accept render callbacks; a plugin supplies values and chooses when to refresh them.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ Rust tests live alongside their modules; native LSP integration checks are in [`
|
|||||||
- Jump-history checks cover cross-buffer/same-document navigation, search and picker recording, counts and repeated visits, bounded history, branch truncation, closed buffers, edited-coordinate clamping, dirty text/undo preservation, Ctrl+i/Tab aliases, and custom bindings. Real LSP fixtures cover definition round-trips and delayed replies after leaving and returning to the same source location.
|
- Jump-history checks cover cross-buffer/same-document navigation, search and picker recording, counts and repeated visits, bounded history, branch truncation, closed buffers, edited-coordinate clamping, dirty text/undo preservation, Ctrl+i/Tab aliases, and custom bindings. Real LSP fixtures cover definition round-trips and delayed replies after leaving and returning to the same source location.
|
||||||
- Tab checks cover shared edits/history with independent pane layouts, focused views, explorers and viewports; cross-tab close guards, immutable snapshots, hidden comparison refresh/result routing, and separate repository workers. Rendered checks cover sanitized/dirty titles, active-tab visibility in narrow terminals, overlay row reservation, and exclusion of hidden diff geometry from palette polling and viewport reporting.
|
- Tab checks cover shared edits/history with independent pane layouts, focused views, explorers and viewports; cross-tab close guards, immutable snapshots, hidden comparison refresh/result routing, and separate repository workers. Rendered checks cover sanitized/dirty titles, active-tab visibility in narrow terminals, overlay row reservation, and exclusion of hidden diff geometry from palette polling and viewport reporting.
|
||||||
- Session and Ruby checks exercise real Ruby execution, command/capability ownership, effect validation, configuration fallback, plugin retirement, crash/timeout recovery, and inherited-pipe cleanup.
|
- Session and Ruby checks exercise real Ruby execution, command/capability ownership, effect validation, configuration fallback, plugin retirement, crash/timeout recovery, and inherited-pipe cleanup.
|
||||||
- For chrome changes, exercise anchor/template validation, padding/edge bounds, named startup values, and atomic live label batches. Rendering regressions should cover full-row center anchoring, terminal-cell glyph widths, empty custom-segment visibility, active-tab preservation, and padding inside styled edges. Labels are session-wide presentation state: test cross-tab/read-only updates and unchanged-value redraw behavior without collecting real battery data or invoking render callbacks.
|
- For chrome changes, exercise anchor/template validation, padding/edge bounds, all builtin/custom segments in either bar, named startup values, and atomic live label batches. Rendering regressions should cover swapped bars across ordinary/split/diff/selector views, full-row center anchoring, terminal-cell glyph widths, empty-content row removal, tabs-only singleton hiding in either position, active-tab preservation, padding inside styled edges, and separate prompt geometry. Labels are session-wide presentation state: test cross-tab/read-only updates and unchanged-value redraw behavior without collecting real battery data or invoking render callbacks.
|
||||||
- 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 caret remains visible. A protocol-state regression verifies that the terminal is not left in synchronized-output mode while the next frame is computed.
|
||||||
- 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.
|
||||||
|
|||||||
+4
-2
@@ -218,9 +218,11 @@ Use `:tab.new` to open the current buffer in a new, focused tab with one pane. E
|
|||||||
| `:tab.close` | `config.core.tab.close` | Close the current tab and all its panes, protecting unsaved changes |
|
| `:tab.close` | `config.core.tab.close` | Close the current tab and all its panes, protecting unsaved changes |
|
||||||
| `:tab.force_close` | `config.core.tab.force_close` | Explicitly discard exclusively displayed edits and close the tab |
|
| `:tab.force_close` | `config.core.tab.force_close` | Explicitly discard exclusively displayed edits and close the tab |
|
||||||
|
|
||||||
Multiple tabs show a single top row with numbered filenames and dirty markers, with padding inside each tab's styling. By default, the active tab has an accent-colored background fading into the terminal background through `░▒▓` / `▓▒░`, except the first tab has no left fade; inactive tabs remain unfilled. Narrow terminals keep the active title visible. New/close actions have no default keybinding; for example, `config.bind "leader t", config.core.tab.new`.
|
By default, multiple tabs show a single top row with numbered filenames and dirty markers, with padding inside each tab's styling. The active tab has an accent-colored background fading into the terminal background through `░▒▓` / `▓▒░`, except the first tab has no left fade; inactive tabs remain unfilled. Narrow terminals keep the active title visible. New/close actions have no default keybinding; for example, `config.bind "leader t", config.core.tab.new`.
|
||||||
|
|
||||||
Both the top tabline and bottom statusline use [configurable ordered segments](configuration.md#tabline-and-statusline-segments), anchored left, truly centered on the full row, or anchored right. Title templates, internal padding, and styled edge glyphs can be configured independently. Custom named values such as a battery label are session-wide and can update through plugins without editing buffers. A visible custom segment also shows the top row with a single tab; empty values hide their custom segments. Narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped.
|
The [top and bottom bars](configuration.md#top-and-bottom-bars) accept the same builtin and custom segments, anchored left, truly centered on the full row, or anchored right. Either bar can show tabs or status information, with independent title templates, internal padding, and styled edge glyphs. Defaults keep tabs above and status below; the command/search prompt stays separate. Empty content consumes no row, and a bar whose only visible content is tabs is hidden with a single tab in either position. Custom named values such as a battery label are session-wide and can update through plugins without editing buffers; empty values hide their segments. Both bars share collision handling: narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped.
|
||||||
|
|
||||||
|
At very small heights, both bars yield to the separate active prompt and a document row. If only one bar row fits while both have content, the top bar receives it.
|
||||||
|
|
||||||
Closing a tab refuses dirty buffers whose final displayed owner would disappear, including a comparison's working and return buffers. Buffers displayed in another tab remain open with their edits, even on force-close. Other hidden buffers remain loaded. Closing the last tab quits only if every open buffer is clean; force-close quits without saving.
|
Closing a tab refuses dirty buffers whose final displayed owner would disappear, including a comparison's working and return buffers. Buffers displayed in another tab remain open with their edits, even on force-close. Other hidden buffers remain loaded. Closing the last tab quits only if every open buffer is clean; force-close quits without saving.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -48,7 +48,7 @@ Navigation carries the source `buffer_id`/`revision`, destination `path`/`row`/`
|
|||||||
|
|
||||||
## Named label updates
|
## Named label updates
|
||||||
|
|
||||||
`editor.label(name, text)` sets a session-wide named value initialized by `config.labels`. Place that name in a [tabline or statusline anchor group](configuration.md#tabline-and-statusline-segments) to display it. This presentation effect does not edit text, move carets, change undo history, or dismiss completion. It also works in read-only diff views.
|
`editor.label(name, text)` sets a session-wide named value initialized by `config.labels`. Place that name in a [top or bottom bar anchor group](configuration.md#top-and-bottom-bars) to display it. This presentation effect does not edit text, move carets, change undo history, or dismiss completion. It also works in read-only diff views.
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Vamp.plugin "battery_label"
|
Vamp.plugin "battery_label"
|
||||||
@@ -72,7 +72,7 @@ Names match `[A-Za-z_][A-Za-z0-9_]*` and text must be a string. Control characte
|
|||||||
|
|
||||||
## Messaging
|
## Messaging
|
||||||
|
|
||||||
Rust and Ruby exchange UTF-8, newline-delimited JSON objects with `version: 1`. Startup returns `manifest` (name, required capabilities, commands, async opt-in) or `config` (plugin paths/options, bindings, chrome configuration, and named labels). `invoke` requests carry an ID, command, arguments, and context; `result` replies carry the matching ID and typed effects (`insert`, `replace`, `message`, `label`). A `label` effect carries `name` and `text` strings. Async hosts also receive `document`/`shutdown` and send `update` frames with optional request IDs and typed presentation/navigation effects. Errors use `error` frames. Unknown fields, wrong versions, and mismatched replies are rejected. Frames and aggregate effect text are bounded to 4 MiB.
|
Rust and Ruby exchange UTF-8, newline-delimited JSON objects with `version: 1`. Startup returns `manifest` (name, required capabilities, commands, async opt-in) or `config` (plugin paths/options, bindings, `top_bar` and `bottom_bar` chrome maps, and named labels). `invoke` requests carry an ID, command, arguments, and context; `result` replies carry the matching ID and typed effects (`insert`, `replace`, `message`, `label`). A `label` effect carries `name` and `text` strings. Async hosts also receive `document`/`shutdown` and send `update` frames with optional request IDs and typed presentation/navigation effects. Errors use `error` frames. Unknown fields, wrong versions, and mismatched replies are rejected. Frames and aggregate effect text are bounded to 4 MiB.
|
||||||
|
|
||||||
Before loading a script, Rust supplies a bounded startup JSON envelope containing `core_actions` and `options`. The action catalog initializes the Ruby interface; only the separate user-options object is exposed through `Vamp.options`, including when user options contain a key named `core_actions`.
|
Before loading a script, Rust supplies a bounded startup JSON envelope containing `core_actions` and `options`. The action catalog initializes the Ruby interface; only the separate user-options object is exposed through `Vamp.options`, including when user options contain a key named `core_actions`.
|
||||||
|
|
||||||
|
|||||||
+8
-7
@@ -7,7 +7,14 @@ Vamp.configure do |config|
|
|||||||
config.file_explorer_side = "right"
|
config.file_explorer_side = "right"
|
||||||
config.file_explorer_view = "tree"
|
config.file_explorer_view = "tree"
|
||||||
config.file_picker_view = "tree"
|
config.file_picker_view = "tree"
|
||||||
config.tabline = {
|
# Status above, tabs below; either bar accepts all segments and style options.
|
||||||
|
config.bar.top = {
|
||||||
|
left: [:mode, :path, :language, :diagnostics, :selections, :count, :pending, :buffer],
|
||||||
|
center: [], right: [:position],
|
||||||
|
mode: { padding: 1, leading_edge: "", trailing_edge: "▓▒░" },
|
||||||
|
position: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
||||||
|
}
|
||||||
|
config.bar.bottom = {
|
||||||
left: [:tabs], center: [], right: [:battery],
|
left: [:tabs], center: [], right: [:battery],
|
||||||
tabs: {
|
tabs: {
|
||||||
title: "{index} {name}{modified}", padding: 1,
|
title: "{index} {name}{modified}", padding: 1,
|
||||||
@@ -15,12 +22,6 @@ Vamp.configure do |config|
|
|||||||
},
|
},
|
||||||
battery: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
battery: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
||||||
}
|
}
|
||||||
config.statusline = {
|
|
||||||
left: [:mode, :path, :language, :diagnostics, :selections, :count, :pending, :buffer],
|
|
||||||
center: [], right: [:position],
|
|
||||||
mode: { padding: 1, leading_edge: "", trailing_edge: "▓▒░" },
|
|
||||||
position: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
|
|
||||||
}
|
|
||||||
config.labels = { battery: "bat 38%" } # Static example; plugins update via editor.label(name, text)
|
config.labels = { battery: "bat 38%" } # Static example; plugins update via editor.label(name, text)
|
||||||
config.bind "ctrl+j", config.core.down, mode: config.modes.selection
|
config.bind "ctrl+j", config.core.down, mode: config.modes.selection
|
||||||
config.bind "ctrl+k", config.core.up, mode: config.modes.selection
|
config.bind "ctrl+k", config.core.up, mode: config.modes.selection
|
||||||
|
|||||||
+61
-32
@@ -1,7 +1,7 @@
|
|||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
const STATUS_SEGMENTS: &[&str] = &[
|
const DEFAULT_BOTTOM_SEGMENTS: &[&str] = &[
|
||||||
"mode",
|
"mode",
|
||||||
"pending",
|
"pending",
|
||||||
"buffer",
|
"buffer",
|
||||||
@@ -43,7 +43,7 @@ impl Default for SegmentConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ChromeLine {
|
impl ChromeLine {
|
||||||
pub fn tabline() -> Self {
|
pub fn top() -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: vec!["tabs".into()],
|
left: vec!["tabs".into()],
|
||||||
center: Vec::new(),
|
center: Vec::new(),
|
||||||
@@ -52,15 +52,15 @@ impl ChromeLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn statusline() -> Self {
|
pub fn bottom() -> Self {
|
||||||
Self {
|
Self {
|
||||||
left: STATUS_SEGMENTS[..STATUS_SEGMENTS.len() - 1]
|
left: DEFAULT_BOTTOM_SEGMENTS[..DEFAULT_BOTTOM_SEGMENTS.len() - 1]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|name| (*name).into())
|
.map(|name| (*name).into())
|
||||||
.collect(),
|
.collect(),
|
||||||
center: Vec::new(),
|
center: Vec::new(),
|
||||||
right: vec!["position".into()],
|
right: vec!["position".into()],
|
||||||
segments: STATUS_SEGMENTS
|
segments: DEFAULT_BOTTOM_SEGMENTS
|
||||||
.iter()
|
.iter()
|
||||||
.map(|name| ((*name).into(), default_segment(name)))
|
.map(|name| ((*name).into(), default_segment(name)))
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -72,7 +72,7 @@ impl ChromeLine {
|
|||||||
self.segments.get(name).expect("validated chrome segment")
|
self.segments.get(name).expect("validated chrome segment")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate(&mut self, tabline: bool) -> Result<(), String> {
|
pub fn validate(&mut self) -> Result<(), String> {
|
||||||
for name in self
|
for name in self
|
||||||
.left
|
.left
|
||||||
.iter()
|
.iter()
|
||||||
@@ -81,13 +81,6 @@ impl ChromeLine {
|
|||||||
.chain(self.segments.keys())
|
.chain(self.segments.keys())
|
||||||
{
|
{
|
||||||
validate_name(name)?;
|
validate_name(name)?;
|
||||||
if (tabline && STATUS_SEGMENTS.contains(&name.as_str())) || (!tabline && name == "tabs")
|
|
||||||
{
|
|
||||||
return Err(format!(
|
|
||||||
"Chrome segment {name:?} is only available in the {}",
|
|
||||||
if tabline { "statusline" } else { "tabline" }
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (name, segment) in &self.segments {
|
for (name, segment) in &self.segments {
|
||||||
if segment.padding > 64 {
|
if segment.padding > 64 {
|
||||||
@@ -188,7 +181,7 @@ struct SegmentOptions {
|
|||||||
|
|
||||||
fn deserialize_line<'de, D: Deserializer<'de>>(
|
fn deserialize_line<'de, D: Deserializer<'de>>(
|
||||||
deserializer: D,
|
deserializer: D,
|
||||||
tabline: bool,
|
mut line: ChromeLine,
|
||||||
) -> Result<ChromeLine, D::Error> {
|
) -> Result<ChromeLine, D::Error> {
|
||||||
// Decode through a value so explicit null is rejected rather than treated as an omitted option.
|
// Decode through a value so explicit null is rejected rather than treated as an omitted option.
|
||||||
let value = serde_json::Value::deserialize(deserializer)?;
|
let value = serde_json::Value::deserialize(deserializer)?;
|
||||||
@@ -207,11 +200,6 @@ fn deserialize_line<'de, D: Deserializer<'de>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let options: LineOptions = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
|
let options: LineOptions = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
|
||||||
let mut line = if tabline {
|
|
||||||
ChromeLine::tabline()
|
|
||||||
} else {
|
|
||||||
ChromeLine::statusline()
|
|
||||||
};
|
|
||||||
if let Some(left) = options.left {
|
if let Some(left) = options.left {
|
||||||
line.left = left;
|
line.left = left;
|
||||||
}
|
}
|
||||||
@@ -242,16 +230,16 @@ fn deserialize_line<'de, D: Deserializer<'de>>(
|
|||||||
segment.first_leading_edge = Some(edge);
|
segment.first_leading_edge = Some(edge);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
line.validate(tabline).map_err(serde::de::Error::custom)?;
|
line.validate().map_err(serde::de::Error::custom)?;
|
||||||
Ok(line)
|
Ok(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tabline<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
|
pub fn top<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
|
||||||
deserialize_line(deserializer, true)
|
deserialize_line(deserializer, ChromeLine::top())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn statusline<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
|
pub fn bottom<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
|
||||||
deserialize_line(deserializer, false)
|
deserialize_line(deserializer, ChromeLine::bottom())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn labels<'de, D: Deserializer<'de>>(
|
pub fn labels<'de, D: Deserializer<'de>>(
|
||||||
@@ -270,9 +258,9 @@ mod tests {
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Lines {
|
struct Lines {
|
||||||
#[serde(deserialize_with = "tabline")]
|
#[serde(deserialize_with = "top")]
|
||||||
top: ChromeLine,
|
top: ChromeLine,
|
||||||
#[serde(deserialize_with = "statusline")]
|
#[serde(deserialize_with = "bottom")]
|
||||||
bottom: ChromeLine,
|
bottom: ChromeLine,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,11 +278,51 @@ mod tests {
|
|||||||
assert_eq!(lines.bottom.segment("workspace"), &SegmentConfig::default());
|
assert_eq!(lines.bottom.segment("workspace"), &SegmentConfig::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn both_positions_accept_all_builtins_and_custom_styles() {
|
||||||
|
let names = [
|
||||||
|
"tabs",
|
||||||
|
"mode",
|
||||||
|
"pending",
|
||||||
|
"buffer",
|
||||||
|
"diagnostics",
|
||||||
|
"path",
|
||||||
|
"selections",
|
||||||
|
"count",
|
||||||
|
"language",
|
||||||
|
"position",
|
||||||
|
"workspace",
|
||||||
|
];
|
||||||
|
let options = serde_json::json!({
|
||||||
|
"left": names,
|
||||||
|
"center": ["language"],
|
||||||
|
"right": ["tabs"],
|
||||||
|
"tabs": {"title": "{index}:{name}:{path}{modified}", "padding": 0},
|
||||||
|
"mode": {"leading_edge": "<", "trailing_edge": ">"},
|
||||||
|
"workspace": {"first_leading_edge": ""}
|
||||||
|
});
|
||||||
|
let lines: Lines = serde_json::from_value(serde_json::json!({
|
||||||
|
"top": options,
|
||||||
|
"bottom": options,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
for line in [&lines.top, &lines.bottom] {
|
||||||
|
assert_eq!(line.left, names);
|
||||||
|
assert_eq!(line.center, ["language"]);
|
||||||
|
assert_eq!(line.right, ["tabs"]);
|
||||||
|
assert_eq!(line.segment("tabs").padding, 0);
|
||||||
|
assert_eq!(line.segment("mode").leading_edge, "<");
|
||||||
|
assert_eq!(
|
||||||
|
line.segment("workspace").first_leading_edge.as_deref(),
|
||||||
|
Some("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn malformed_chrome_is_rejected_at_the_boundary() {
|
fn malformed_chrome_is_rejected_at_the_boundary() {
|
||||||
for top in [
|
for options in [
|
||||||
serde_json::json!({"left": ["bad-name"]}),
|
serde_json::json!({"left": ["bad-name"]}),
|
||||||
serde_json::json!({"left": ["mode"]}),
|
|
||||||
serde_json::json!({"left": null}),
|
serde_json::json!({"left": null}),
|
||||||
serde_json::json!({"tabs": {"padding": 65}}),
|
serde_json::json!({"tabs": {"padding": 65}}),
|
||||||
serde_json::json!({"tabs": {"padding": null}}),
|
serde_json::json!({"tabs": {"padding": null}}),
|
||||||
@@ -304,10 +332,11 @@ mod tests {
|
|||||||
serde_json::json!({"tabs": {"title": "name}"}}),
|
serde_json::json!({"tabs": {"title": "name}"}}),
|
||||||
serde_json::json!({"tabs": {"colour": "red"}}),
|
serde_json::json!({"tabs": {"colour": "red"}}),
|
||||||
] {
|
] {
|
||||||
assert!(
|
for position in ["top", "bottom"] {
|
||||||
serde_json::from_value::<Lines>(serde_json::json!({"top": top, "bottom": {}}))
|
let mut frame = serde_json::json!({"top": {}, "bottom": {}});
|
||||||
.is_err()
|
frame[position] = options.clone();
|
||||||
);
|
assert!(serde_json::from_value::<Lines>(frame).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-25
@@ -1351,8 +1351,8 @@ pub struct Editor {
|
|||||||
owned_system_clipboard: Option<(String, Vec<String>, bool)>,
|
owned_system_clipboard: Option<(String, Vec<String>, bool)>,
|
||||||
last_edit: Option<EditRecipe>,
|
last_edit: Option<EditRecipe>,
|
||||||
message: String,
|
message: String,
|
||||||
tabline: ChromeLine,
|
top_bar: ChromeLine,
|
||||||
statusline: ChromeLine,
|
bottom_bar: ChromeLine,
|
||||||
labels: BTreeMap<String, String>,
|
labels: BTreeMap<String, String>,
|
||||||
quit: bool,
|
quit: bool,
|
||||||
capabilities: Vec<String>,
|
capabilities: Vec<String>,
|
||||||
@@ -1439,8 +1439,8 @@ impl Editor {
|
|||||||
owned_system_clipboard: None,
|
owned_system_clipboard: None,
|
||||||
last_edit: None,
|
last_edit: None,
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
tabline: ChromeLine::tabline(),
|
top_bar: ChromeLine::top(),
|
||||||
statusline: ChromeLine::statusline(),
|
bottom_bar: ChromeLine::bottom(),
|
||||||
labels: BTreeMap::new(),
|
labels: BTreeMap::new(),
|
||||||
quit: false,
|
quit: false,
|
||||||
capabilities,
|
capabilities,
|
||||||
@@ -3831,11 +3831,11 @@ impl Editor {
|
|||||||
pub fn message(&self) -> &str {
|
pub fn message(&self) -> &str {
|
||||||
&self.message
|
&self.message
|
||||||
}
|
}
|
||||||
pub fn tabline_config(&self) -> &ChromeLine {
|
pub fn top_bar_config(&self) -> &ChromeLine {
|
||||||
&self.tabline
|
&self.top_bar
|
||||||
}
|
}
|
||||||
pub fn statusline_config(&self) -> &ChromeLine {
|
pub fn bottom_bar_config(&self) -> &ChromeLine {
|
||||||
&self.statusline
|
&self.bottom_bar
|
||||||
}
|
}
|
||||||
pub fn label(&self, name: &str) -> &str {
|
pub fn label(&self, name: &str) -> &str {
|
||||||
self.labels.get(name).map_or("", String::as_str)
|
self.labels.get(name).map_or("", String::as_str)
|
||||||
@@ -3881,12 +3881,12 @@ impl Editor {
|
|||||||
|
|
||||||
pub fn set_chrome_config(
|
pub fn set_chrome_config(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut tabline: ChromeLine,
|
mut top: ChromeLine,
|
||||||
mut statusline: ChromeLine,
|
mut bottom: ChromeLine,
|
||||||
labels: BTreeMap<String, String>,
|
labels: BTreeMap<String, String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
tabline.validate(true)?;
|
top.validate()?;
|
||||||
statusline.validate(false)?;
|
bottom.validate()?;
|
||||||
let mut total = 0usize;
|
let mut total = 0usize;
|
||||||
for (name, text) in &labels {
|
for (name, text) in &labels {
|
||||||
validate_label(name, text)?;
|
validate_label(name, text)?;
|
||||||
@@ -3898,8 +3898,8 @@ impl Editor {
|
|||||||
return Err("Labels exceed 4 MiB".into());
|
return Err("Labels exceed 4 MiB".into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabline = tabline;
|
self.top_bar = top;
|
||||||
self.statusline = statusline;
|
self.bottom_bar = bottom;
|
||||||
self.labels = labels
|
self.labels = labels
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, text)| (name, sanitize_label(text)))
|
.map(|(name, text)| (name, sanitize_label(text)))
|
||||||
@@ -17749,9 +17749,11 @@ mod tests {
|
|||||||
fn chrome_installation_and_invalid_label_batches_are_atomic() {
|
fn chrome_installation_and_invalid_label_batches_are_atomic() {
|
||||||
let mut editor = editor();
|
let mut editor = editor();
|
||||||
editor.set_label("battery".into(), "ready".into()).unwrap();
|
editor.set_label("battery".into(), "ready".into()).unwrap();
|
||||||
let mut top = ChromeLine::tabline();
|
let mut top = ChromeLine::top();
|
||||||
|
top.left = vec!["mode".into(), "language".into()];
|
||||||
top.center.push("workspace".into());
|
top.center.push("workspace".into());
|
||||||
let bottom = ChromeLine::statusline();
|
let mut bottom = ChromeLine::bottom();
|
||||||
|
bottom.left = vec!["tabs".into()];
|
||||||
editor
|
editor
|
||||||
.set_chrome_config(
|
.set_chrome_config(
|
||||||
top.clone(),
|
top.clone(),
|
||||||
@@ -17764,18 +17766,34 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(editor.label("workspace"), "tools�");
|
assert_eq!(editor.label("workspace"), "tools�");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.tabline_config().segment("workspace"),
|
editor.top_bar_config().segment("workspace"),
|
||||||
&crate::chrome::SegmentConfig::default()
|
&crate::chrome::SegmentConfig::default()
|
||||||
);
|
);
|
||||||
let installed = editor.tabline_config().clone();
|
let installed_top = editor.top_bar_config().clone();
|
||||||
top.segments.get_mut("tabs").unwrap().title = "{missing}".into();
|
let installed_bottom = editor.bottom_bar_config().clone();
|
||||||
assert!(
|
assert_eq!(installed_top.left, ["mode", "language"]);
|
||||||
editor
|
assert_eq!(installed_bottom.left, ["tabs"]);
|
||||||
.set_chrome_config(top, bottom, BTreeMap::new())
|
top.right.push("uninstalled".into());
|
||||||
.is_err()
|
bottom.center.push("uninstalled".into());
|
||||||
);
|
let mut invalid_top = top.clone();
|
||||||
assert_eq!(editor.tabline_config(), &installed);
|
invalid_top.segments.get_mut("tabs").unwrap().title = "{missing}".into();
|
||||||
|
let mut invalid_bottom = bottom.clone();
|
||||||
|
invalid_bottom.segments.get_mut("mode").unwrap().padding = 65;
|
||||||
|
for (top, bottom, labels) in [
|
||||||
|
(invalid_top, bottom.clone(), BTreeMap::new()),
|
||||||
|
(top.clone(), invalid_bottom, BTreeMap::new()),
|
||||||
|
(
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
BTreeMap::from([("bad-name".into(), "invalid".into())]),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
assert!(editor.set_chrome_config(top, bottom, labels).is_err());
|
||||||
|
assert_eq!(editor.top_bar_config(), &installed_top);
|
||||||
|
assert_eq!(editor.bottom_bar_config(), &installed_bottom);
|
||||||
assert_eq!(editor.label("battery"), "ready");
|
assert_eq!(editor.label("battery"), "ready");
|
||||||
|
assert_eq!(editor.label("workspace"), "tools�");
|
||||||
|
}
|
||||||
assert!(
|
assert!(
|
||||||
editor
|
editor
|
||||||
.apply(vec![
|
.apply(vec![
|
||||||
|
|||||||
+82
-60
@@ -38,8 +38,8 @@ pub struct Config {
|
|||||||
pub file_picker_view: String,
|
pub file_picker_view: String,
|
||||||
pub file_explorer_view: String,
|
pub file_explorer_view: String,
|
||||||
pub clipboard_mode: String,
|
pub clipboard_mode: String,
|
||||||
pub tabline: ChromeLine,
|
pub top_bar: ChromeLine,
|
||||||
pub statusline: ChromeLine,
|
pub bottom_bar: ChromeLine,
|
||||||
pub labels: BTreeMap<String, String>,
|
pub labels: BTreeMap<String, String>,
|
||||||
pub diff_context_lines: usize,
|
pub diff_context_lines: usize,
|
||||||
pub diff_expand_lines: usize,
|
pub diff_expand_lines: usize,
|
||||||
@@ -77,8 +77,8 @@ impl Default for Config {
|
|||||||
file_picker_view: "tree".into(),
|
file_picker_view: "tree".into(),
|
||||||
file_explorer_view: "tree".into(),
|
file_explorer_view: "tree".into(),
|
||||||
clipboard_mode: "system".into(),
|
clipboard_mode: "system".into(),
|
||||||
tabline: ChromeLine::tabline(),
|
top_bar: ChromeLine::top(),
|
||||||
statusline: ChromeLine::statusline(),
|
bottom_bar: ChromeLine::bottom(),
|
||||||
labels: BTreeMap::new(),
|
labels: BTreeMap::new(),
|
||||||
diff_context_lines: 10,
|
diff_context_lines: 10,
|
||||||
diff_expand_lines: 15,
|
diff_expand_lines: 15,
|
||||||
@@ -102,8 +102,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
|||||||
file_picker_view,
|
file_picker_view,
|
||||||
file_explorer_view,
|
file_explorer_view,
|
||||||
clipboard_mode,
|
clipboard_mode,
|
||||||
tabline,
|
top_bar,
|
||||||
statusline,
|
bottom_bar,
|
||||||
labels,
|
labels,
|
||||||
diff_context_lines,
|
diff_context_lines,
|
||||||
diff_expand_lines,
|
diff_expand_lines,
|
||||||
@@ -129,8 +129,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
|||||||
config.file_picker_view = file_picker_view;
|
config.file_picker_view = file_picker_view;
|
||||||
config.file_explorer_view = file_explorer_view;
|
config.file_explorer_view = file_explorer_view;
|
||||||
config.clipboard_mode = clipboard_mode;
|
config.clipboard_mode = clipboard_mode;
|
||||||
config.tabline = tabline;
|
config.top_bar = top_bar;
|
||||||
config.statusline = statusline;
|
config.bottom_bar = bottom_bar;
|
||||||
config.labels = labels;
|
config.labels = labels;
|
||||||
config.diff_context_lines = diff_context_lines;
|
config.diff_context_lines = diff_context_lines;
|
||||||
config.diff_expand_lines = diff_expand_lines;
|
config.diff_expand_lines = diff_expand_lines;
|
||||||
@@ -450,10 +450,10 @@ enum Response {
|
|||||||
file_picker_view: String,
|
file_picker_view: String,
|
||||||
file_explorer_view: String,
|
file_explorer_view: String,
|
||||||
clipboard_mode: String,
|
clipboard_mode: String,
|
||||||
#[serde(deserialize_with = "crate::chrome::tabline")]
|
#[serde(deserialize_with = "crate::chrome::top")]
|
||||||
tabline: ChromeLine,
|
top_bar: ChromeLine,
|
||||||
#[serde(deserialize_with = "crate::chrome::statusline")]
|
#[serde(deserialize_with = "crate::chrome::bottom")]
|
||||||
statusline: ChromeLine,
|
bottom_bar: ChromeLine,
|
||||||
#[serde(deserialize_with = "crate::chrome::labels")]
|
#[serde(deserialize_with = "crate::chrome::labels")]
|
||||||
labels: BTreeMap<String, String>,
|
labels: BTreeMap<String, String>,
|
||||||
#[serde(deserialize_with = "diff_context_lines")]
|
#[serde(deserialize_with = "diff_context_lines")]
|
||||||
@@ -1181,9 +1181,9 @@ end
|
|||||||
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
|
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
|
||||||
}
|
}
|
||||||
for frame in [
|
for frame in [
|
||||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","tabline":{},"statusline":{},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
|
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
|
||||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","tabline":{},"statusline":{},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
|
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
|
||||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","tabline":{},"statusline":{},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
|
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
|
||||||
] {
|
] {
|
||||||
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
||||||
}
|
}
|
||||||
@@ -1196,69 +1196,91 @@ end
|
|||||||
"config.rb",
|
"config.rb",
|
||||||
r#"
|
r#"
|
||||||
Vamp.configure do |config|
|
Vamp.configure do |config|
|
||||||
config.tabline = {left: [:tabs], center: ["workspace"], right: [:battery],
|
config.bar.top = {left: [:tabs], center: ["workspace"], right: [:battery],
|
||||||
tabs: {title: "T{index}:{path}{modified}", padding: 0, first_leading_edge: ""},
|
tabs: {title: "T{index}:{path}{modified}", padding: 0, first_leading_edge: ""},
|
||||||
battery: {leading_edge: "界", trailing_edge: ""}}
|
battery: {leading_edge: "界", trailing_edge: ""}}
|
||||||
config.statusline = {"left" => [], center: [:workspace]}
|
config.bar.bottom = {"left" => [], center: [:workspace]}
|
||||||
config.labels = {battery: "bat 38%", "workspace" => "界 e\u0301 👩🚀\t\n\e"}
|
config.labels = {battery: "bat 38%", "workspace" => "界 e\u0301 👩🚀\t\n\e"}
|
||||||
end
|
end
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
let loaded = load_config(&path).unwrap();
|
let loaded = load_config(&path).unwrap();
|
||||||
assert_eq!(loaded.tabline.left, ["tabs"]);
|
assert_eq!(loaded.top_bar.left, ["tabs"]);
|
||||||
assert_eq!(loaded.tabline.center, ["workspace"]);
|
assert_eq!(loaded.top_bar.center, ["workspace"]);
|
||||||
assert_eq!(loaded.tabline.right, ["battery"]);
|
assert_eq!(loaded.top_bar.right, ["battery"]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded.tabline.segment("tabs").title,
|
loaded.top_bar.segment("tabs").title,
|
||||||
"T{index}:{path}{modified}"
|
"T{index}:{path}{modified}"
|
||||||
);
|
);
|
||||||
assert_eq!(loaded.tabline.segment("tabs").padding, 0);
|
assert_eq!(loaded.top_bar.segment("tabs").padding, 0);
|
||||||
assert_eq!(loaded.tabline.segment("tabs").trailing_edge, "▓▒░");
|
assert_eq!(loaded.top_bar.segment("tabs").trailing_edge, "▓▒░");
|
||||||
assert_eq!(loaded.tabline.segment("battery").leading_edge, "界");
|
assert_eq!(loaded.top_bar.segment("battery").leading_edge, "界");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded.tabline.segment("workspace"),
|
loaded.top_bar.segment("workspace"),
|
||||||
&crate::chrome::SegmentConfig::default()
|
&crate::chrome::SegmentConfig::default()
|
||||||
);
|
);
|
||||||
assert!(loaded.statusline.left.is_empty());
|
assert!(loaded.bottom_bar.left.is_empty());
|
||||||
assert_eq!(loaded.statusline.right, ["position"]);
|
assert_eq!(loaded.bottom_bar.right, ["position"]);
|
||||||
assert_eq!(loaded.labels["battery"], "bat 38%");
|
assert_eq!(loaded.labels["battery"], "bat 38%");
|
||||||
assert_eq!(loaded.labels["workspace"], "界 e\u{301} 👩🚀\t\n\u{1b}");
|
assert_eq!(loaded.labels["workspace"], "界 e\u{301} 👩🚀\t\n\u{1b}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bar_namespace_is_stable_and_accepts_swapped_builtins() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let path = fixture.file(
|
||||||
|
"config.rb",
|
||||||
|
r#"
|
||||||
|
Vamp.configure do |config|
|
||||||
|
bar = config.bar
|
||||||
|
raise "bar namespace must be stable" unless bar.equal?(config.bar)
|
||||||
|
bar.top = {left: [:mode, :language], right: [], mode: {padding: 0}}
|
||||||
|
config.bar.bottom = {left: [:tabs], right: [], tabs: {title: "{name}{modified}"}}
|
||||||
|
end
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let loaded = load_config(&path).unwrap();
|
||||||
|
assert_eq!(loaded.top_bar.left, ["mode", "language"]);
|
||||||
|
assert_eq!(loaded.top_bar.segment("mode").padding, 0);
|
||||||
|
assert_eq!(loaded.top_bar.segment("language").padding, 1);
|
||||||
|
assert_eq!(loaded.bottom_bar.left, ["tabs"]);
|
||||||
|
assert_eq!(loaded.bottom_bar.segment("tabs").title, "{name}{modified}");
|
||||||
|
|
||||||
|
for setter in ["bar", "tabline", "statusline"] {
|
||||||
|
let path = fixture.file(
|
||||||
|
"config.rb",
|
||||||
|
&format!("Vamp.configure {{ |config| config.{setter} = {{}} }}"),
|
||||||
|
);
|
||||||
|
let error = load_config(&path).unwrap_err();
|
||||||
|
assert!(error.contains("undefined method"), "{setter}: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chrome_configuration_rejects_invalid_types_names_and_templates() {
|
fn chrome_configuration_rejects_invalid_types_names_and_templates() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
for (setting, value, expected) in [
|
for (setting, value, expected) in [
|
||||||
("tabline", "[]", "must be a Hash"),
|
("bar.top", "[]", "must be a Hash"),
|
||||||
("tabline", "{left: :tabs}", "must be an Array"),
|
("bar.bottom", "[]", "must be a Hash"),
|
||||||
("tabline", "{left: [7]}", "must be a String"),
|
("bar.top", "{left: :tabs}", "must be an Array"),
|
||||||
("tabline", "{left: [:bad_name!]}", "must match"),
|
("bar.top", "{left: [7]}", "must be a String"),
|
||||||
|
("bar.top", "{left: [:bad_name!]}", "must match"),
|
||||||
(
|
(
|
||||||
"tabline",
|
"bar.bottom",
|
||||||
"{left: [:mode]}",
|
|
||||||
"only available in the statusline",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"statusline",
|
|
||||||
"{right: [:tabs]}",
|
|
||||||
"only available in the tabline",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"tabline",
|
|
||||||
"{tabs: {title: '{unknown}'}}",
|
"{tabs: {title: '{unknown}'}}",
|
||||||
"unknown title placeholder",
|
"unknown title placeholder",
|
||||||
),
|
),
|
||||||
("tabline", r#"{tabs: {leading_edge: "\n"}}"#, "single-line"),
|
("bar.top", r#"{mode: {leading_edge: "\n"}}"#, "single-line"),
|
||||||
("tabline", "{tabs: {padding: 65}}", "between 0 and 64"),
|
("bar.top", "{language: {padding: 65}}", "between 0 and 64"),
|
||||||
("tabline", "{tabs: {padding: '1'}}", "must be an Integer"),
|
("bar.bottom", "{tabs: {padding: '1'}}", "must be an Integer"),
|
||||||
("tabline", "{tabs: {title: nil}}", "must be a String"),
|
("bar.top", "{mode: {title: nil}}", "must be a String"),
|
||||||
(
|
(
|
||||||
"tabline",
|
"bar.bottom",
|
||||||
"{tabs: {color: 'red'}}",
|
"{tabs: {color: 'red'}}",
|
||||||
"unknown chrome style option",
|
"unknown chrome style option",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"tabline",
|
"bar.top",
|
||||||
"{left: [], 'left' => []}",
|
"{left: [], 'left' => []}",
|
||||||
"duplicate chrome key",
|
"duplicate chrome key",
|
||||||
),
|
),
|
||||||
@@ -1310,7 +1332,7 @@ end
|
|||||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||||
"tabline": {}, "statusline": {}, "labels": {},
|
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||||
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7
|
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7
|
||||||
});
|
});
|
||||||
for setting in ["diff_context_lines", "diff_expand_lines"] {
|
for setting in ["diff_context_lines", "diff_expand_lines"] {
|
||||||
@@ -1723,7 +1745,7 @@ end"#,
|
|||||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||||
"tabline": {}, "statusline": {}, "labels": {},
|
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||||
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15
|
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15
|
||||||
});
|
});
|
||||||
let Response::Config { lsp, .. } =
|
let Response::Config { lsp, .. } =
|
||||||
@@ -1806,18 +1828,18 @@ end"#,
|
|||||||
for mutation in [
|
for mutation in [
|
||||||
"return '['",
|
"return '['",
|
||||||
"frame.delete('lsp')",
|
"frame.delete('lsp')",
|
||||||
"frame.delete('tabline')",
|
"frame.delete('top_bar')",
|
||||||
"frame.delete('statusline')",
|
"frame.delete('bottom_bar')",
|
||||||
"frame.delete('labels')",
|
"frame.delete('labels')",
|
||||||
"frame['tabline'] = 7",
|
"frame['top_bar'] = 7",
|
||||||
"frame['statusline'] = []",
|
"frame['bottom_bar'] = []",
|
||||||
"frame['labels'] = {'battery' => 7}",
|
"frame['labels'] = {'battery' => 7}",
|
||||||
"frame['tabline'] = {'tabs' => {'title' => '{unknown}'}}",
|
"frame['bottom_bar'] = {'tabs' => {'title' => '{unknown}'}}",
|
||||||
"frame['tabline'] = {'tabs' => {'leading_edge' => \"\\n\"}}",
|
"frame['top_bar'] = {'mode' => {'leading_edge' => \"\\n\"}}",
|
||||||
"frame['tabline'] = {'left' => nil}",
|
"frame['top_bar'] = {'left' => nil}",
|
||||||
"frame['tabline'] = {'left' => ['bad-name']}",
|
"frame['bottom_bar'] = {'left' => ['bad-name']}",
|
||||||
"frame['tabline'] = {'tabs' => {'padding' => '1'}}",
|
"frame['bottom_bar'] = {'tabs' => {'padding' => '1'}}",
|
||||||
"frame['tabline'] = {'tabs' => {'color' => 'red'}}",
|
"frame['top_bar'] = {'language' => {'color' => 'red'}}",
|
||||||
"frame['labels'] = {'bad-name' => ''}",
|
"frame['labels'] = {'bad-name' => ''}",
|
||||||
"frame['leader'] = 7",
|
"frame['leader'] = 7",
|
||||||
"frame['unexpected'] = true",
|
"frame['unexpected'] = true",
|
||||||
|
|||||||
+69
-56
@@ -12,11 +12,77 @@ module Vamp
|
|||||||
name
|
name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @param value [Hash{String, Symbol => Object}]
|
||||||
|
# @return [Hash{String => Object}]
|
||||||
|
def self.chrome_hash!(value)
|
||||||
|
raise TypeError, "chrome options must be a Hash" unless value.is_a?(Hash)
|
||||||
|
value.each_with_object({}) do |(key, item), result|
|
||||||
|
key = key.to_s if key.is_a?(Symbol)
|
||||||
|
key = string!(key, "chrome key")
|
||||||
|
raise ArgumentError, "duplicate chrome key: #{key}" if result.key?(key)
|
||||||
|
result[key] = item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param options [Hash{String, Symbol => Object}]
|
||||||
|
# @return [Hash{String => Object}]
|
||||||
|
def self.chrome_line!(options)
|
||||||
|
chrome_hash!(options).to_h do |name, value|
|
||||||
|
if %w[left center right].include?(name)
|
||||||
|
raise TypeError, "#{name} must be an Array" unless value.is_a?(Array)
|
||||||
|
[name, value.map { |item| chrome_name!(item) }]
|
||||||
|
else
|
||||||
|
chrome_name!(name)
|
||||||
|
style = chrome_hash!(value).to_h do |option, item|
|
||||||
|
case option
|
||||||
|
when "padding"
|
||||||
|
unless item.is_a?(Integer) && item.between?(0, 64)
|
||||||
|
raise ArgumentError, "padding must be an Integer between 0 and 64"
|
||||||
|
end
|
||||||
|
when "title", "leading_edge", "trailing_edge", "first_leading_edge"
|
||||||
|
item = string!(item, option)
|
||||||
|
else
|
||||||
|
raise ArgumentError, "unknown chrome style option: #{option}"
|
||||||
|
end
|
||||||
|
[option, item]
|
||||||
|
end
|
||||||
|
[name, style]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Collects options for the independently configured top and bottom bars.
|
||||||
|
class BarConfig
|
||||||
|
# @return [Hash{String => Object}]
|
||||||
|
attr_reader :top, :bottom
|
||||||
|
|
||||||
|
# @return [void]
|
||||||
|
def initialize
|
||||||
|
@top = {}
|
||||||
|
@bottom = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param options [Hash{String, Symbol => Object}]
|
||||||
|
# @return [Hash{String => Object}]
|
||||||
|
def top=(options)
|
||||||
|
@top = Vamp.chrome_line!(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param options [Hash{String, Symbol => Object}]
|
||||||
|
# @return [Hash{String => Object}]
|
||||||
|
def bottom=(options)
|
||||||
|
@bottom = Vamp.chrome_line!(options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Collects and validates the Ruby startup configuration DSL.
|
# Collects and validates the Ruby startup configuration DSL.
|
||||||
class Config
|
class Config
|
||||||
# @return [Hash{String => Boolean, Hash{String, Symbol => Object}}] language presets/options
|
# @return [Hash{String => Boolean, Hash{String, Symbol => Object}}] language presets/options
|
||||||
attr_reader :lsp
|
attr_reader :lsp
|
||||||
|
|
||||||
|
# @return [Vamp::BarConfig]
|
||||||
|
attr_reader :bar
|
||||||
|
|
||||||
# @return [void]
|
# @return [void]
|
||||||
def initialize
|
def initialize
|
||||||
@plugins = []
|
@plugins = []
|
||||||
@@ -27,8 +93,7 @@ module Vamp
|
|||||||
@file_picker_view = "tree"
|
@file_picker_view = "tree"
|
||||||
@file_explorer_view = "tree"
|
@file_explorer_view = "tree"
|
||||||
@clipboard_mode = "system"
|
@clipboard_mode = "system"
|
||||||
@tabline = {}
|
@bar = BarConfig.new
|
||||||
@statusline = {}
|
|
||||||
@labels = {}
|
@labels = {}
|
||||||
@diff_context_lines = 10
|
@diff_context_lines = 10
|
||||||
@diff_expand_lines = 15
|
@diff_expand_lines = 15
|
||||||
@@ -76,22 +141,10 @@ module Vamp
|
|||||||
@clipboard_mode = nonempty_string!(mode, "clipboard mode")
|
@clipboard_mode = nonempty_string!(mode, "clipboard mode")
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param options [Hash{String, Symbol => Object}]
|
|
||||||
# @return [Hash]
|
|
||||||
def tabline=(options)
|
|
||||||
@tabline = chrome_line!(options)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param options [Hash{String, Symbol => Object}]
|
|
||||||
# @return [Hash]
|
|
||||||
def statusline=(options)
|
|
||||||
@statusline = chrome_line!(options)
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param values [Hash{String, Symbol => String}]
|
# @param values [Hash{String, Symbol => String}]
|
||||||
# @return [Hash]
|
# @return [Hash]
|
||||||
def labels=(values)
|
def labels=(values)
|
||||||
@labels = chrome_hash!(values).to_h do |name, text|
|
@labels = Vamp.chrome_hash!(values).to_h do |name, text|
|
||||||
[Vamp.chrome_name!(name), Vamp.string!(text, "label text")]
|
[Vamp.chrome_name!(name), Vamp.string!(text, "label text")]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -139,51 +192,11 @@ module Vamp
|
|||||||
{ "version" => 1, "type" => "config", "plugins" => @plugins, "lsp" => lsp_options, "bindings" => @bindings,
|
{ "version" => 1, "type" => "config", "plugins" => @plugins, "lsp" => lsp_options, "bindings" => @bindings,
|
||||||
"leader" => @leader, "file_explorer_side" => @file_explorer_side, "file_picker_view" => @file_picker_view,
|
"leader" => @leader, "file_explorer_side" => @file_explorer_side, "file_picker_view" => @file_picker_view,
|
||||||
"file_explorer_view" => @file_explorer_view, "clipboard_mode" => @clipboard_mode,
|
"file_explorer_view" => @file_explorer_view, "clipboard_mode" => @clipboard_mode,
|
||||||
"tabline" => @tabline, "statusline" => @statusline, "labels" => @labels,
|
"top_bar" => @bar.top, "bottom_bar" => @bar.bottom, "labels" => @labels,
|
||||||
"diff_context_lines" => @diff_context_lines, "diff_expand_lines" => @diff_expand_lines }
|
"diff_context_lines" => @diff_context_lines, "diff_expand_lines" => @diff_expand_lines }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
# @param value [Hash{String, Symbol => Object}]
|
|
||||||
# @return [Hash{String => Object}]
|
|
||||||
def chrome_hash!(value)
|
|
||||||
raise TypeError, "chrome options must be a Hash" unless value.is_a?(Hash)
|
|
||||||
value.each_with_object({}) do |(key, item), result|
|
|
||||||
key = key.to_s if key.is_a?(Symbol)
|
|
||||||
key = Vamp.string!(key, "chrome key")
|
|
||||||
raise ArgumentError, "duplicate chrome key: #{key}" if result.key?(key)
|
|
||||||
result[key] = item
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param options [Hash{String, Symbol => Object}]
|
|
||||||
# @return [Hash{String => Object}]
|
|
||||||
def chrome_line!(options)
|
|
||||||
chrome_hash!(options).to_h do |name, value|
|
|
||||||
if %w[left center right].include?(name)
|
|
||||||
raise TypeError, "#{name} must be an Array" unless value.is_a?(Array)
|
|
||||||
[name, value.map { |item| Vamp.chrome_name!(item) }]
|
|
||||||
else
|
|
||||||
Vamp.chrome_name!(name)
|
|
||||||
style = chrome_hash!(value).to_h do |option, item|
|
|
||||||
case option
|
|
||||||
when "padding"
|
|
||||||
unless item.is_a?(Integer) && item.between?(0, 64)
|
|
||||||
raise ArgumentError, "padding must be an Integer between 0 and 64"
|
|
||||||
end
|
|
||||||
when "title", "leading_edge", "trailing_edge", "first_leading_edge"
|
|
||||||
item = Vamp.string!(item, option)
|
|
||||||
else
|
|
||||||
raise ArgumentError, "unknown chrome style option: #{option}"
|
|
||||||
end
|
|
||||||
[option, item]
|
|
||||||
end
|
|
||||||
[name, style]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# @param value [Integer]
|
# @param value [Integer]
|
||||||
# @param label [String]
|
# @param label [String]
|
||||||
# @param minimum [Integer]
|
# @param minimum [Integer]
|
||||||
|
|||||||
+7
-5
@@ -221,7 +221,7 @@ impl EditorSession {
|
|||||||
}
|
}
|
||||||
editor.set_diff_context_options(config.diff_context_lines, config.diff_expand_lines);
|
editor.set_diff_context_options(config.diff_context_lines, config.diff_expand_lines);
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
editor.set_chrome_config(config.tabline, config.statusline, config.labels)
|
editor.set_chrome_config(config.top_bar, config.bottom_bar, config.labels)
|
||||||
{
|
{
|
||||||
notices.push(format!("chrome: {error}"));
|
notices.push(format!("chrome: {error}"));
|
||||||
}
|
}
|
||||||
@@ -3016,16 +3016,18 @@ end
|
|||||||
let session = fixture.open(
|
let session = fixture.open(
|
||||||
r#"
|
r#"
|
||||||
Vamp.configure do |config|
|
Vamp.configure do |config|
|
||||||
config.tabline = {right: [:battery]}
|
config.bar.top = {left: [:mode, :language], right: [:battery]}
|
||||||
config.statusline = {center: [:workspace]}
|
config.bar.bottom = {left: [:tabs], center: [:workspace]}
|
||||||
config.labels = {battery: "bat 38%", workspace: "tools\n"}
|
config.labels = {battery: "bat 38%", workspace: "tools\n"}
|
||||||
end
|
end
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
assert_eq!(session.editor.label("battery"), "bat 38%");
|
assert_eq!(session.editor.label("battery"), "bat 38%");
|
||||||
assert_eq!(session.editor.label("workspace"), "tools�");
|
assert_eq!(session.editor.label("workspace"), "tools�");
|
||||||
assert_eq!(session.editor.tabline_config().right, ["battery"]);
|
assert_eq!(session.editor.top_bar_config().left, ["mode", "language"]);
|
||||||
assert_eq!(session.editor.statusline_config().center, ["workspace"]);
|
assert_eq!(session.editor.top_bar_config().right, ["battery"]);
|
||||||
|
assert_eq!(session.editor.bottom_bar_config().left, ["tabs"]);
|
||||||
|
assert_eq!(session.editor.bottom_bar_config().center, ["workspace"]);
|
||||||
assert_eq!(session.editor.lines(), ["source"]);
|
assert_eq!(session.editor.lines(), ["source"]);
|
||||||
assert_eq!(session.editor.text_revision(), 0);
|
assert_eq!(session.editor.text_revision(), 0);
|
||||||
assert!(!session.editor.dirty());
|
assert!(!session.editor.dirty());
|
||||||
|
|||||||
+472
-129
@@ -406,15 +406,9 @@ impl BufferViewports {
|
|||||||
self.parked
|
self.parked
|
||||||
.retain(|id, _| editor.pane_buffer_id(*id).is_some());
|
.retain(|id, _| editor.pane_buffer_id(*id).is_some());
|
||||||
self.syntax.retain(|id, _| editor.has_buffer(*id));
|
self.syntax.retain(|id, _| editor.has_buffer(*id));
|
||||||
let area = frame.area();
|
let syntax = self.syntax.entry(editor.buffer_id()).or_default();
|
||||||
let area = draw_tab_bar(frame, editor, area);
|
let [full_workspace, prompt] = draw_bars(frame, editor, frame.area(), syntax);
|
||||||
let multiple = editor.pane_layout().len() > 1;
|
let multiple = editor.pane_layout().len() > 1;
|
||||||
let chrome = if !multiple && editor.diff().is_some() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(document_layout(area, editor))
|
|
||||||
};
|
|
||||||
let full_workspace = chrome.map_or(area, |areas| areas[0]);
|
|
||||||
let diff_header = editor
|
let diff_header = editor
|
||||||
.diff()
|
.diff()
|
||||||
.filter(|diff| !multiple && diff.phase == Phase::View)
|
.filter(|diff| !multiple && diff.phase == Phase::View)
|
||||||
@@ -452,6 +446,10 @@ impl BufferViewports {
|
|||||||
.visit_rects(pane_rect(workspace), |id, area| {
|
.visit_rects(pane_rect(workspace), |id, area| {
|
||||||
self.rects.push((id, terminal_rect(area)));
|
self.rects.push((id, terminal_rect(area)));
|
||||||
});
|
});
|
||||||
|
// An active prompt still needs its focused render pass when no document row fits.
|
||||||
|
if self.rects.is_empty() && prompt.height > 0 {
|
||||||
|
self.rects.push((focused, workspace));
|
||||||
|
}
|
||||||
// Paint focused overlays last, so an adjacent leaf cannot erase them.
|
// Paint focused overlays last, so an adjacent leaf cannot erase them.
|
||||||
for active_pass in [false, true] {
|
for active_pass in [false, true] {
|
||||||
for index in 0..self.rects.len() {
|
for index in 0..self.rects.len() {
|
||||||
@@ -474,12 +472,11 @@ impl BufferViewports {
|
|||||||
frame.render_widget(block, leaf);
|
frame.render_widget(block, leaf);
|
||||||
leaf = inner;
|
leaf = inner;
|
||||||
}
|
}
|
||||||
let status = chrome
|
let prompt = if id == focused {
|
||||||
.filter(|_| id == focused)
|
prompt
|
||||||
.map_or(Rect::default(), |a| a[1]);
|
} else {
|
||||||
let prompt = chrome
|
Rect::default()
|
||||||
.filter(|_| id == focused)
|
};
|
||||||
.map_or(Rect::default(), |a| a[2]);
|
|
||||||
editor.with_render_pane(id, |view| {
|
editor.with_render_pane(id, |view| {
|
||||||
self.activate_document(view.buffer_id());
|
self.activate_document(view.buffer_id());
|
||||||
self.documents.retain(|id, _| view.has_buffer(*id));
|
self.documents.retain(|id, _| view.has_buffer(*id));
|
||||||
@@ -492,8 +489,8 @@ impl BufferViewports {
|
|||||||
draw_document(
|
draw_document(
|
||||||
frame,
|
frame,
|
||||||
view,
|
view,
|
||||||
[leaf, status, prompt],
|
[leaf, prompt],
|
||||||
area,
|
full_workspace,
|
||||||
top,
|
top,
|
||||||
left,
|
left,
|
||||||
syntax,
|
syntax,
|
||||||
@@ -544,14 +541,20 @@ impl BufferViewports {
|
|||||||
draw_diff(
|
draw_diff(
|
||||||
frame,
|
frame,
|
||||||
diff,
|
diff,
|
||||||
area,
|
full_workspace,
|
||||||
editor.selection_navigation_hint(),
|
editor.selection_navigation_hint(),
|
||||||
editor.file_navigation_hint(),
|
editor.file_navigation_hint(),
|
||||||
&mut self.panels,
|
&mut self.panels,
|
||||||
Some(editor),
|
Some(editor),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
draw_overlays(frame, editor, area, self.panels.caret, &mut self.panels);
|
draw_overlays(
|
||||||
|
frame,
|
||||||
|
editor,
|
||||||
|
full_workspace,
|
||||||
|
self.panels.caret,
|
||||||
|
&mut self.panels,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.diff_visible = self
|
self.diff_visible = self
|
||||||
.panels
|
.panels
|
||||||
@@ -1071,7 +1074,7 @@ fn chrome_items<'a>(
|
|||||||
editor: &'a Editor,
|
editor: &'a Editor,
|
||||||
line: &'a ChromeLine,
|
line: &'a ChromeLine,
|
||||||
names: &[String],
|
names: &[String],
|
||||||
syntax: Option<&SyntaxCache>,
|
syntax: &SyntaxCache,
|
||||||
) -> Vec<ChromeItem<'a>> {
|
) -> Vec<ChromeItem<'a>> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
for name in names {
|
for name in names {
|
||||||
@@ -1100,31 +1103,28 @@ fn chrome_items<'a>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut text = match name.as_str() {
|
let mut text = match name.as_str() {
|
||||||
"mode" if syntax.is_some() => Cow::Owned(if editor.visual_linewise() {
|
"mode" => Cow::Owned(if editor.visual_linewise() {
|
||||||
"VISUAL LINE".into()
|
"VISUAL LINE".into()
|
||||||
} else {
|
} else {
|
||||||
editor.mode().to_string()
|
editor.mode().to_string()
|
||||||
}),
|
}),
|
||||||
"path" if syntax.is_some() => Cow::Owned(chrome_title(
|
"path" => Cow::Owned(chrome_title(
|
||||||
"{path}{modified}",
|
"{path}{modified}",
|
||||||
editor.tab_index() + 1,
|
editor.tab_index() + 1,
|
||||||
editor.path(),
|
editor.path(),
|
||||||
editor.dirty(),
|
editor.dirty(),
|
||||||
)),
|
)),
|
||||||
"position" if syntax.is_some() => Cow::Owned(format!(
|
"position" => Cow::Owned(format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
editor.cursor().row + 1,
|
editor.cursor().row + 1,
|
||||||
editor.cursor().column + 1
|
editor.cursor().column + 1
|
||||||
)),
|
)),
|
||||||
"language" if syntax.is_some() => {
|
"language" => Cow::Owned(if let Some(error) = syntax.error() {
|
||||||
let syntax = syntax.unwrap();
|
|
||||||
Cow::Owned(if let Some(error) = syntax.error() {
|
|
||||||
format!("syntax: {error}")
|
format!("syntax: {error}")
|
||||||
} else {
|
} else {
|
||||||
syntax.language().unwrap_or_default().to_owned()
|
syntax.language().unwrap_or_default().to_owned()
|
||||||
})
|
}),
|
||||||
}
|
"diagnostics" => {
|
||||||
"diagnostics" if syntax.is_some() => {
|
|
||||||
let mut counts = [0; 4];
|
let mut counts = [0; 4];
|
||||||
for diagnostic in editor.diagnostics() {
|
for diagnostic in editor.diagnostics() {
|
||||||
counts[diagnostic_severity(diagnostic.severity).0] += 1;
|
counts[diagnostic_severity(diagnostic.severity).0] += 1;
|
||||||
@@ -1140,18 +1140,18 @@ fn chrome_items<'a>(
|
|||||||
}
|
}
|
||||||
Cow::Owned(text)
|
Cow::Owned(text)
|
||||||
}
|
}
|
||||||
"selections" if syntax.is_some() => Cow::Owned(if editor.selection_count() > 1 {
|
"selections" => Cow::Owned(if editor.selection_count() > 1 {
|
||||||
format!("{} selections", editor.selection_count())
|
format!("{} selections", editor.selection_count())
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
}),
|
}),
|
||||||
"count" if syntax.is_some() => Cow::Owned(
|
"count" => Cow::Owned(
|
||||||
editor
|
editor
|
||||||
.count()
|
.count()
|
||||||
.map(|count| format!("count: {count}"))
|
.map(|count| format!("count: {count}"))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
),
|
),
|
||||||
"pending" if syntax.is_some() => {
|
"pending" => {
|
||||||
let mut text = editor.sequence_pending().unwrap_or_default().to_owned();
|
let mut text = editor.sequence_pending().unwrap_or_default().to_owned();
|
||||||
for pending in [
|
for pending in [
|
||||||
editor.g_pending().then_some('g'),
|
editor.g_pending().then_some('g'),
|
||||||
@@ -1168,7 +1168,7 @@ fn chrome_items<'a>(
|
|||||||
}
|
}
|
||||||
Cow::Owned(text)
|
Cow::Owned(text)
|
||||||
}
|
}
|
||||||
"buffer" if syntax.is_some() => Cow::Owned(if editor.buffer_count() > 1 {
|
"buffer" => Cow::Owned(if editor.buffer_count() > 1 {
|
||||||
format!("[buffer {}/{}]", editor.buffer_id(), editor.buffer_count())
|
format!("[buffer {}/{}]", editor.buffer_id(), editor.buffer_count())
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
@@ -1348,47 +1348,69 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec<ChromeItem<'_>>; 3], area: Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
|
fn chrome_groups<'a>(
|
||||||
if area.height == 0 {
|
editor: &'a Editor,
|
||||||
return area;
|
line: &'a ChromeLine,
|
||||||
}
|
syntax: &SyntaxCache,
|
||||||
let line = editor.tabline_config();
|
) -> [Vec<ChromeItem<'a>>; 3] {
|
||||||
let anchors = [&line.left, &line.center, &line.right];
|
[&line.left, &line.center, &line.right].map(|names| chrome_items(editor, line, names, syntax))
|
||||||
if !anchors.iter().flat_map(|names| names.iter()).any(|name| {
|
|
||||||
if name == "tabs" {
|
|
||||||
editor.tab_count() > 1
|
|
||||||
} else {
|
|
||||||
!editor.label(name).is_empty()
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
return area;
|
|
||||||
}
|
|
||||||
let groups = anchors.map(|names| chrome_items(editor, line, names, None));
|
|
||||||
if !groups.iter().flatten().any(|item| item.tab.is_none())
|
|
||||||
&& !(editor.tab_count() > 1 && groups.iter().any(|group| !group.is_empty()))
|
|
||||||
{
|
|
||||||
return area;
|
|
||||||
}
|
|
||||||
draw_chrome(frame, groups, Rect::new(area.x, area.y, area.width, 1));
|
|
||||||
Rect::new(
|
|
||||||
area.x,
|
|
||||||
area.y.saturating_add(1),
|
|
||||||
area.width,
|
|
||||||
area.height - 1,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn document_layout(area: Rect, editor: &Editor) -> [Rect; 3] {
|
fn chrome_visible(editor: &Editor, groups: &[Vec<ChromeItem<'_>>; 3]) -> bool {
|
||||||
|
groups.iter().flatten().any(|item| item.tab.is_none())
|
||||||
|
|| (editor.tab_count() > 1 && groups.iter().any(|group| !group.is_empty()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn document_layout(area: Rect, editor: &Editor, visible: [bool; 2]) -> [Rect; 4] {
|
||||||
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
|
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
|
||||||
let dock_focused = editor
|
let dock_focused = editor
|
||||||
.file_picker()
|
.file_picker()
|
||||||
.is_some_and(|picker| picker.focused && picker.presentation == FilePresentation::Docked);
|
.is_some_and(|picker| picker.focused && picker.presentation == FilePresentation::Docked);
|
||||||
Layout::vertical([
|
let prompt =
|
||||||
Constraint::Fill(1),
|
u16::from(editor.diff().is_none() && (area.height >= 2 || (prompting && !dock_focused)))
|
||||||
Constraint::Length(u16::from(area.height >= 3)),
|
.min(area.height);
|
||||||
Constraint::Length(u16::from(area.height >= 2 || (prompting && !dock_focused))),
|
// Both bars yield to the separate prompt and the last document row.
|
||||||
])
|
let available = area.height.saturating_sub(prompt + 1);
|
||||||
.areas(area)
|
let top = u16::from(visible[0]).min(available);
|
||||||
|
let bottom = u16::from(visible[1]).min(available - top);
|
||||||
|
let workspace = Rect::new(
|
||||||
|
area.x,
|
||||||
|
area.y.saturating_add(top),
|
||||||
|
area.width,
|
||||||
|
area.height - top - bottom - prompt,
|
||||||
|
);
|
||||||
|
[
|
||||||
|
Rect::new(area.x, area.y, area.width, top),
|
||||||
|
workspace,
|
||||||
|
Rect::new(area.x, workspace.bottom(), area.width, bottom),
|
||||||
|
Rect::new(area.x, area.bottom() - prompt, area.width, prompt),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_bars(
|
||||||
|
frame: &mut Frame,
|
||||||
|
editor: &Editor,
|
||||||
|
area: Rect,
|
||||||
|
syntax: &mut SyntaxCache,
|
||||||
|
) -> [Rect; 2] {
|
||||||
|
if area.width == 0 || area.height == 0 {
|
||||||
|
let [_, workspace, _, prompt] = document_layout(area, editor, [false; 2]);
|
||||||
|
return [workspace, prompt];
|
||||||
|
}
|
||||||
|
// Resolve the active buffer's language before computing either bar's visibility.
|
||||||
|
syntax.update_visible(editor.lines(), editor.path(), editor.text_revision(), &[]);
|
||||||
|
let groups = [
|
||||||
|
chrome_groups(editor, editor.top_bar_config(), syntax),
|
||||||
|
chrome_groups(editor, editor.bottom_bar_config(), syntax),
|
||||||
|
];
|
||||||
|
let visible = groups
|
||||||
|
.each_ref()
|
||||||
|
.map(|groups| chrome_visible(editor, groups));
|
||||||
|
let [top, workspace, bottom, prompt] = document_layout(area, editor, visible);
|
||||||
|
for (groups, area) in groups.into_iter().zip([top, bottom]) {
|
||||||
|
draw_chrome(frame, groups, area);
|
||||||
|
}
|
||||||
|
[workspace, prompt]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1402,14 +1424,9 @@ fn draw(
|
|||||||
panels: &mut WheelPanels,
|
panels: &mut WheelPanels,
|
||||||
) {
|
) {
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
let area = draw_tab_bar(frame, editor, area);
|
let areas = draw_bars(frame, editor, area, syntax);
|
||||||
let areas = if editor.diff().is_some() {
|
|
||||||
[area, Rect::default(), Rect::default()]
|
|
||||||
} else {
|
|
||||||
document_layout(area, editor)
|
|
||||||
};
|
|
||||||
draw_document(
|
draw_document(
|
||||||
frame, editor, areas, area, top, left, syntax, document, panels,
|
frame, editor, areas, areas[0], top, left, syntax, document, panels,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1417,7 +1434,7 @@ fn draw(
|
|||||||
fn draw_document(
|
fn draw_document(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
editor: &Editor,
|
editor: &Editor,
|
||||||
[workspace, status_area, prompt_area]: [Rect; 3],
|
[workspace, prompt_area]: [Rect; 2],
|
||||||
overlay_area: Rect,
|
overlay_area: Rect,
|
||||||
top: &mut usize,
|
top: &mut usize,
|
||||||
left: &mut usize,
|
left: &mut usize,
|
||||||
@@ -1430,7 +1447,6 @@ fn draw_document(
|
|||||||
let area = workspace;
|
let area = workspace;
|
||||||
let focused = editor.render_pane_focused();
|
let focused = editor.render_pane_focused();
|
||||||
if let Some(diff) = editor.diff() {
|
if let Some(diff) = editor.diff() {
|
||||||
draw_status(frame, editor, syntax, status_area);
|
|
||||||
if panels.workspace_managed && diff.phase != Phase::View {
|
if panels.workspace_managed && diff.phase != Phase::View {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1446,7 +1462,7 @@ fn draw_document(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
panels.diff_syntax = None;
|
panels.diff_syntax = None;
|
||||||
if overlay_area.width == 0 || overlay_area.height == 0 {
|
if overlay_area.width == 0 || (overlay_area.height == 0 && prompt_area.height == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
|
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
|
||||||
@@ -1759,7 +1775,6 @@ fn draw_document(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
draw_status(frame, editor, syntax, status_area);
|
|
||||||
if prompting && prompt_area.height > 0 && prompt_area.width > 0 {
|
if prompting && prompt_area.height > 0 && prompt_area.width > 0 {
|
||||||
let prefix = if editor.mode() == Mode::Search {
|
let prefix = if editor.mode() == Mode::Search {
|
||||||
'/'
|
'/'
|
||||||
@@ -1895,13 +1910,6 @@ fn draw_overlays(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status(frame: &mut Frame, editor: &Editor, syntax: &SyntaxCache, status_area: Rect) {
|
|
||||||
let line = editor.statusline_config();
|
|
||||||
let groups = [&line.left, &line.center, &line.right]
|
|
||||||
.map(|names| chrome_items(editor, line, names, Some(syntax)));
|
|
||||||
draw_chrome(frame, groups, status_area);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DiffGeometry {
|
struct DiffGeometry {
|
||||||
regions: [(Rect, Focus); 3],
|
regions: [(Rect, Focus); 3],
|
||||||
viewport: Option<(u64, usize, usize, usize, usize)>,
|
viewport: Option<(u64, usize, usize, usize, usize)>,
|
||||||
@@ -5447,8 +5455,8 @@ mod tests {
|
|||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
update: impl FnOnce(&mut ChromeLine, &mut ChromeLine),
|
update: impl FnOnce(&mut ChromeLine, &mut ChromeLine),
|
||||||
) {
|
) {
|
||||||
let mut top = editor.tabline_config().clone();
|
let mut top = editor.top_bar_config().clone();
|
||||||
let mut bottom = editor.statusline_config().clone();
|
let mut bottom = editor.bottom_bar_config().clone();
|
||||||
update(&mut top, &mut bottom);
|
update(&mut top, &mut bottom);
|
||||||
editor
|
editor
|
||||||
.set_chrome_config(top, bottom, BTreeMap::new())
|
.set_chrome_config(top, bottom, BTreeMap::new())
|
||||||
@@ -5490,7 +5498,11 @@ mod tests {
|
|||||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 3)).unwrap();
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 3)).unwrap();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
draw_tab_bar(frame, &editor, frame.area());
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||||
|
frame.area(),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
|
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
|
||||||
@@ -5508,7 +5520,11 @@ mod tests {
|
|||||||
});
|
});
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
draw_tab_bar(frame, &editor, frame.area());
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||||
|
frame.area(),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
|
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
|
||||||
@@ -5554,8 +5570,16 @@ mod tests {
|
|||||||
let syntax = SyntaxCache::new();
|
let syntax = SyntaxCache::new();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
draw_tab_bar(frame, &editor, frame.area());
|
draw_chrome(
|
||||||
draw_status(frame, &editor, &syntax, Rect::new(3, 2, 45, 1));
|
frame,
|
||||||
|
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||||
|
frame.area(),
|
||||||
|
);
|
||||||
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, editor.bottom_bar_config(), &syntax),
|
||||||
|
Rect::new(3, 2, 45, 1),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let buffer = terminal.backend().buffer();
|
let buffer = terminal.backend().buffer();
|
||||||
@@ -5570,8 +5594,16 @@ mod tests {
|
|||||||
editor.set_label("left".into(), "L".repeat(30)).unwrap();
|
editor.set_label("left".into(), "L".repeat(30)).unwrap();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
draw_tab_bar(frame, &editor, frame.area());
|
draw_chrome(
|
||||||
draw_status(frame, &editor, &syntax, Rect::new(3, 2, 45, 1));
|
frame,
|
||||||
|
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||||
|
frame.area(),
|
||||||
|
);
|
||||||
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, editor.bottom_bar_config(), &syntax),
|
||||||
|
Rect::new(3, 2, 45, 1),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for (y, x, width) in [(0, 0, 51), (2, 3, 45)] {
|
for (y, x, width) in [(0, 0, 51), (2, 3, 45)] {
|
||||||
@@ -5585,7 +5617,7 @@ mod tests {
|
|||||||
fn active_tabs_keep_their_anchor_and_text_when_optional_groups_collide() {
|
fn active_tabs_keep_their_anchor_and_text_when_optional_groups_collide() {
|
||||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
editor.execute("tab.new");
|
editor.execute("tab.new");
|
||||||
configure_chrome(&mut editor, |top, _| {
|
configure_chrome(&mut editor, |top, bottom| {
|
||||||
top.left = vec!["prefix".into()];
|
top.left = vec!["prefix".into()];
|
||||||
top.center = vec!["tabs".into()];
|
top.center = vec!["tabs".into()];
|
||||||
top.right = vec!["suffix".into()];
|
top.right = vec!["suffix".into()];
|
||||||
@@ -5594,6 +5626,7 @@ mod tests {
|
|||||||
tabs.padding = 64;
|
tabs.padding = 64;
|
||||||
tabs.leading_edge = "界".repeat(8);
|
tabs.leading_edge = "界".repeat(8);
|
||||||
tabs.trailing_edge = "界".repeat(8);
|
tabs.trailing_edge = "界".repeat(8);
|
||||||
|
*bottom = top.clone();
|
||||||
});
|
});
|
||||||
editor
|
editor
|
||||||
.set_label("prefix".into(), "LEFT".repeat(32))
|
.set_label("prefix".into(), "LEFT".repeat(32))
|
||||||
@@ -5605,29 +5638,42 @@ mod tests {
|
|||||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 2)).unwrap();
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 2)).unwrap();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
draw_tab_bar(frame, &editor, frame.area());
|
for (y, line) in [editor.top_bar_config(), editor.bottom_bar_config()]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, line, &SyntaxCache::new()),
|
||||||
|
Rect::new(0, y as u16, width, 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if width > 0 {
|
for y in 0..2 {
|
||||||
let row = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
if width == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let row = pane_screen(&terminal, Rect::new(0, y, width, 1));
|
||||||
assert!(
|
assert!(
|
||||||
row.trim()
|
row.trim()
|
||||||
.starts_with(if width == 1 { "2" } else { "2:active" }),
|
.starts_with(if width == 1 { "2" } else { "2:active" }),
|
||||||
"{row}"
|
"{row}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.backend().buffer()[((width - width.min(8)) / 2, 0)].symbol(),
|
terminal.backend().buffer()[((width - width.min(8)) / 2, y)].symbol(),
|
||||||
"2"
|
"2"
|
||||||
);
|
);
|
||||||
assert!(!row.contains("LEFT") && !row.contains("RIGHT"), "{row}");
|
assert!(!row.contains("LEFT") && !row.contains("RIGHT"), "{row}");
|
||||||
assert!(!row.contains('界'), "{row}");
|
assert!(!row.contains('界'), "{row}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
configure_chrome(&mut editor, |top, _| {
|
configure_chrome(&mut editor, |top, bottom| {
|
||||||
let tabs = top.segments.get_mut("tabs").unwrap();
|
let tabs = top.segments.get_mut("tabs").unwrap();
|
||||||
tabs.padding = 0;
|
tabs.padding = 0;
|
||||||
tabs.leading_edge.clear();
|
tabs.leading_edge.clear();
|
||||||
tabs.trailing_edge.clear();
|
tabs.trailing_edge.clear();
|
||||||
|
*bottom = top.clone();
|
||||||
});
|
});
|
||||||
editor
|
editor
|
||||||
.set_label("prefix".into(), "LEFT".repeat(32))
|
.set_label("prefix".into(), "LEFT".repeat(32))
|
||||||
@@ -5638,12 +5684,23 @@ mod tests {
|
|||||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 2)).unwrap();
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 2)).unwrap();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
draw_tab_bar(frame, &editor, frame.area());
|
for (y, line) in [editor.top_bar_config(), editor.bottom_bar_config()]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, line, &SyntaxCache::new()),
|
||||||
|
Rect::new(0, y as u16, 40, 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let row = pane_screen(&terminal, Rect::new(0, 0, 40, 1));
|
for y in 0..2 {
|
||||||
|
let row = pane_screen(&terminal, Rect::new(0, y, 40, 1));
|
||||||
assert_eq!(&row[16..24], "2:active");
|
assert_eq!(&row[16..24], "2:active");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chrome_uses_cell_widths_for_independent_edges_and_internal_padding() {
|
fn chrome_uses_cell_widths_for_independent_edges_and_internal_padding() {
|
||||||
@@ -5683,8 +5740,16 @@ mod tests {
|
|||||||
let syntax = SyntaxCache::new();
|
let syntax = SyntaxCache::new();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
draw_tab_bar(frame, &editor, frame.area());
|
draw_chrome(
|
||||||
draw_status(frame, &editor, &syntax, Rect::new(0, 2, 40, 1));
|
frame,
|
||||||
|
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||||
|
frame.area(),
|
||||||
|
);
|
||||||
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, editor.bottom_bar_config(), &syntax),
|
||||||
|
Rect::new(0, 2, 40, 1),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let buffer = terminal.backend().buffer();
|
let buffer = terminal.backend().buffer();
|
||||||
@@ -5726,7 +5791,11 @@ mod tests {
|
|||||||
editor.execute("tab.previous");
|
editor.execute("tab.previous");
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
draw_tab_bar(frame, &editor, frame.area());
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||||
|
frame.area(),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let buffer = terminal.backend().buffer();
|
let buffer = terminal.backend().buffer();
|
||||||
@@ -5738,7 +5807,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_configured_chrome_does_not_allocate_a_tab_row() {
|
fn empty_bar_content_reclaims_both_rows_without_hiding_the_prompt() {
|
||||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
editor.execute("tab.new");
|
editor.execute("tab.new");
|
||||||
configure_chrome(&mut editor, |top, bottom| {
|
configure_chrome(&mut editor, |top, bottom| {
|
||||||
@@ -5755,29 +5824,293 @@ mod tests {
|
|||||||
first_leading_edge: None,
|
first_leading_edge: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
bottom.left.clear();
|
*bottom = top.clone();
|
||||||
bottom.center.clear();
|
|
||||||
bottom.right.clear();
|
|
||||||
});
|
});
|
||||||
|
let mut syntax = SyntaxCache::new();
|
||||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
let area = frame.area();
|
let [workspace, prompt] = draw_bars(frame, &editor, frame.area(), &mut syntax);
|
||||||
assert_eq!(draw_tab_bar(frame, &editor, area), area);
|
assert_eq!(workspace, Rect::new(0, 0, 40, 4));
|
||||||
assert_eq!(document_layout(area, &editor)[1].height, 1);
|
assert_eq!(prompt, Rect::new(0, 4, 40, 1));
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
editor.set_label("empty".into(), "present".into()).unwrap();
|
editor.set_label("empty".into(), "present".into()).unwrap();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
let area = frame.area();
|
let [workspace, prompt] = draw_bars(frame, &editor, frame.area(), &mut syntax);
|
||||||
assert_eq!(draw_tab_bar(frame, &editor, area).y, 1);
|
assert_eq!(workspace, Rect::new(0, 1, 40, 2));
|
||||||
|
assert_eq!(prompt, Rect::new(0, 4, 40, 1));
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let title = pane_screen(&terminal, Rect::new(0, 0, 40, 1));
|
for y in [0, 3] {
|
||||||
|
let title = pane_screen(&terminal, Rect::new(0, y, 40, 1));
|
||||||
assert!(title.contains("Not visible without a value"), "{title}");
|
assert!(title.contains("Not visible without a value"), "{title}");
|
||||||
assert!(!title.contains('▓'), "{title}");
|
assert!(!title.contains('▓'), "{title}");
|
||||||
}
|
}
|
||||||
|
editor.handle(Input::Char(':'));
|
||||||
|
terminal
|
||||||
|
.draw(|frame| {
|
||||||
|
let [workspace, prompt] = draw_bars(frame, &editor, frame.area(), &mut syntax);
|
||||||
|
assert_eq!(workspace.height, 2);
|
||||||
|
assert_eq!(prompt, Rect::new(0, 4, 40, 1));
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tiny_bars_yield_to_document_and_prompt_identically_in_either_position() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
let path = directory.file("source.txt", "DOCUMENT");
|
||||||
|
for segment in ["tabs", "mode"] {
|
||||||
|
for at_top in [false, true] {
|
||||||
|
for prompting in [false, true] {
|
||||||
|
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||||
|
editor.execute("tab.new");
|
||||||
|
configure_chrome(&mut editor, |top, bottom| {
|
||||||
|
for line in [&mut *top, &mut *bottom] {
|
||||||
|
line.left.clear();
|
||||||
|
line.center.clear();
|
||||||
|
line.right.clear();
|
||||||
|
}
|
||||||
|
let line = if at_top { top } else { bottom };
|
||||||
|
line.left = vec![segment.into()];
|
||||||
|
});
|
||||||
|
if prompting {
|
||||||
|
editor.handle(Input::Char(':'));
|
||||||
|
}
|
||||||
|
for height in 1..=4 {
|
||||||
|
let area = Rect::new(3, 2, 80, height);
|
||||||
|
let mut terminal = Terminal::with_options(
|
||||||
|
ratatui::backend::TestBackend::new(100, 10),
|
||||||
|
ratatui::TerminalOptions {
|
||||||
|
viewport: ratatui::Viewport::Fixed(area),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut views = BufferViewports::default();
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
let bar_text = match segment {
|
||||||
|
"tabs" => "2 source.txt",
|
||||||
|
_ if prompting => "COMMAND",
|
||||||
|
_ => "NORMAL",
|
||||||
|
};
|
||||||
|
let visible = height >= 3;
|
||||||
|
assert_eq!(screen_text(&terminal).contains(bar_text), visible);
|
||||||
|
let cursor = terminal.get_cursor_position().unwrap();
|
||||||
|
assert!(
|
||||||
|
area.contains(cursor),
|
||||||
|
"{segment} {at_top} prompting={prompting} {area:?}: {cursor:?}"
|
||||||
|
);
|
||||||
|
if prompting {
|
||||||
|
assert_eq!(cursor.y, area.bottom() - 1);
|
||||||
|
assert_eq!(
|
||||||
|
terminal.backend().buffer()[(area.x, cursor.y)].symbol(),
|
||||||
|
":",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !prompting || height > 1 {
|
||||||
|
let viewport = views.document.geometry.unwrap().1;
|
||||||
|
assert!(viewport.height > 0);
|
||||||
|
assert!(pane_screen(&terminal, viewport).contains("DOCUMENT"));
|
||||||
|
}
|
||||||
|
if visible {
|
||||||
|
let y = if at_top { area.y } else { area.bottom() - 2 };
|
||||||
|
assert!(
|
||||||
|
pane_screen(&terminal, Rect::new(area.x, y, area.width, 1))
|
||||||
|
.contains(bar_text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tabs_only_singletons_hide_in_either_bar_and_reappear_with_another_tab() {
|
||||||
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
configure_chrome(&mut editor, |top, bottom| *bottom = top.clone());
|
||||||
|
let mut views = BufferViewports::default();
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(views.rects[0].1, Rect::new(0, 0, 80, 11));
|
||||||
|
assert!(views.scroll_document(&editor, document_wheel(MouseEventKind::ScrollDown, 2, 0),));
|
||||||
|
|
||||||
|
editor.execute("tab.new");
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(views.rects[0].1, Rect::new(0, 1, 80, 9));
|
||||||
|
for y in [0, 10] {
|
||||||
|
assert!(
|
||||||
|
!views.scroll_document(&editor, document_wheel(MouseEventKind::ScrollDown, 2, y),)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let caret = terminal.get_cursor_position().unwrap();
|
||||||
|
assert!(caret.y >= 1 && caret.y < 10);
|
||||||
|
for y in [0, 10] {
|
||||||
|
let bar = pane_screen(&terminal, Rect::new(0, y, 80, 1));
|
||||||
|
assert!(
|
||||||
|
bar.contains("1 [No Name]") && bar.contains("2 [No Name]"),
|
||||||
|
"{bar}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
editor.execute("tab.close");
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(views.rects[0].1, Rect::new(0, 0, 80, 11));
|
||||||
|
assert!(!screen_text(&terminal).contains("[No Name]"));
|
||||||
|
for y in [0, 10] {
|
||||||
|
assert!(
|
||||||
|
views.scroll_document(&editor, document_wheel(MouseEventKind::ScrollDown, 2, y),)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swapped_bars_render_active_metadata_and_language_on_the_first_paint() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
let path = directory.file("active.rs", "fn main() {}");
|
||||||
|
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||||
|
editor.execute("tab.new");
|
||||||
|
editor.handle(Input::Char('i'));
|
||||||
|
editor.handle(Input::Paste("// changed\n".into()));
|
||||||
|
editor.handle(Input::Escape);
|
||||||
|
configure_chrome(&mut editor, std::mem::swap);
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(240, 12)).unwrap();
|
||||||
|
let mut views = BufferViewports::default();
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
let top = pane_screen(&terminal, Rect::new(0, 0, 240, 1));
|
||||||
|
assert!(top.contains("NORMAL"), "{top}");
|
||||||
|
assert!(top.contains(&path.display().to_string()), "{top}");
|
||||||
|
assert!(top.contains("[+]") && top.contains("Rust"), "{top}");
|
||||||
|
let cursor = editor.cursor();
|
||||||
|
assert!(
|
||||||
|
top.contains(&format!("{}:{}", cursor.row + 1, cursor.column + 1)),
|
||||||
|
"{top}"
|
||||||
|
);
|
||||||
|
let bottom = pane_screen(&terminal, Rect::new(0, 10, 240, 1));
|
||||||
|
assert!(
|
||||||
|
bottom.contains("1 active.rs [+]") && bottom.contains("2 active.rs [+]"),
|
||||||
|
"{bottom}"
|
||||||
|
);
|
||||||
|
assert_eq!(views.document.geometry.unwrap().1.y, 1);
|
||||||
|
let before = views.rects[0].1;
|
||||||
|
editor.handle(Input::Char(':'));
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(views.rects[0].1, before);
|
||||||
|
assert!(pane_screen(&terminal, Rect::new(0, 11, 240, 1)).starts_with(':'));
|
||||||
|
assert_eq!(terminal.get_cursor_position().unwrap().y, 11);
|
||||||
|
assert_eq!(pane_screen(&terminal, Rect::new(0, 10, 240, 1)), bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn both_bars_bound_palettes_diff_selectors_docks_and_diff_prompts() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
let mut editor =
|
||||||
|
Editor::open(Some(directory.file("source.rs", "fn main() {}")), vec![]).unwrap();
|
||||||
|
editor.execute("tab.new");
|
||||||
|
configure_chrome(&mut editor, |top, bottom| {
|
||||||
|
std::mem::swap(top, bottom);
|
||||||
|
top.left = vec!["mode".into(), "language".into()];
|
||||||
|
top.center = vec!["workspace".into()];
|
||||||
|
top.right = vec!["position".into()];
|
||||||
|
bottom.right = vec!["battery".into()];
|
||||||
|
});
|
||||||
|
editor
|
||||||
|
.set_label("workspace".into(), "WORKSPACE".into())
|
||||||
|
.unwrap();
|
||||||
|
editor.set_label("battery".into(), "BOTTOM".into()).unwrap();
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 30)).unwrap();
|
||||||
|
let mut views = BufferViewports::default();
|
||||||
|
let assert_bars = |terminal: &Terminal<ratatui::backend::TestBackend>,
|
||||||
|
views: &BufferViewports| {
|
||||||
|
assert!(pane_screen(terminal, Rect::new(0, 0, 120, 1)).contains("WORKSPACE"));
|
||||||
|
let bottom_y = if views.panels.diff.is_some() { 29 } else { 28 };
|
||||||
|
assert!(pane_screen(terminal, Rect::new(0, bottom_y, 120, 1)).contains("BOTTOM"));
|
||||||
|
for (_, rect) in &views.rects {
|
||||||
|
assert!(rect.y >= 1 && rect.bottom() <= bottom_y);
|
||||||
|
}
|
||||||
|
if let Some(geometry) = &views.panels.diff {
|
||||||
|
for (rect, _) in geometry.regions {
|
||||||
|
if rect.height > 0 {
|
||||||
|
assert!(rect.y >= 1 && rect.bottom() <= bottom_y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((rect, _, _)) = views.panels.files {
|
||||||
|
assert!(rect.y >= 1 && rect.bottom() <= bottom_y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert!(pane_screen(&terminal, Rect::new(0, 0, 120, 1)).contains("Rust"));
|
||||||
|
assert_bars(&terminal, &views);
|
||||||
|
editor.handle(Input::Ctrl('p'));
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert!(screen_text(&terminal).contains("Commands"));
|
||||||
|
assert_bars(&terminal, &views);
|
||||||
|
editor.handle(Input::Escape);
|
||||||
|
|
||||||
|
editor.execute("diff");
|
||||||
|
let (id, _) = editor.take_diff_request().unwrap();
|
||||||
|
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert!(screen_text(&terminal).contains("Choose Before"));
|
||||||
|
assert!(views.document.geometry.is_none());
|
||||||
|
assert_bars(&terminal, &views);
|
||||||
|
editor.handle(Input::Enter);
|
||||||
|
editor.handle(Input::Enter);
|
||||||
|
let (id, _) = editor.take_diff_request().unwrap();
|
||||||
|
editor.apply_diff_result(
|
||||||
|
id,
|
||||||
|
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||||
|
root: directory.0.clone(),
|
||||||
|
editable_paths: Default::default(),
|
||||||
|
before: "baseline".into(),
|
||||||
|
after: "Working checkout".into(),
|
||||||
|
files: vec![deletion_file()],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert!(views.panels.files.is_some());
|
||||||
|
assert_bars(&terminal, &views);
|
||||||
|
editor.handle(Input::Char(':'));
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
let cursor = terminal.get_cursor_position().unwrap();
|
||||||
|
assert!(Rect::new(0, 1, 120, 28).contains(cursor), "{cursor:?}");
|
||||||
|
assert_bars(&terminal, &views);
|
||||||
|
editor.handle(Input::Escape);
|
||||||
|
editor.handle(Input::Char(' '));
|
||||||
|
editor.handle(Input::Char('o'));
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
editor.file_picker().unwrap().presentation,
|
||||||
|
FilePresentation::Floating
|
||||||
|
);
|
||||||
|
assert_bars(&terminal, &views);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn narrow_tab_bar_keeps_the_active_title_visible_and_handles_empty_areas() {
|
fn narrow_tab_bar_keeps_the_active_title_visible_and_handles_empty_areas() {
|
||||||
@@ -5802,11 +6135,16 @@ mod tests {
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| views.draw(frame, &mut editor))
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if width > 0 && height > 0 {
|
if width > 0 && height >= 3 {
|
||||||
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
||||||
assert!(title.trim_start().starts_with('3'), "{title}");
|
assert!(title.trim_start().starts_with('3'), "{title}");
|
||||||
assert!(!title.contains("status"), "{title}");
|
assert!(!title.contains("status"), "{title}");
|
||||||
}
|
}
|
||||||
|
if width > 0 && height == 1 {
|
||||||
|
let (_, viewport) = views.document.geometry.unwrap();
|
||||||
|
assert!(viewport.contains(terminal.get_cursor_position().unwrap()));
|
||||||
|
assert!(pane_screen(&terminal, viewport).contains("ACTIVE"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
@@ -5890,7 +6228,11 @@ mod tests {
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
assert_eq!(draw_tab_bar(frame, &editor, area).y, 1);
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||||
|
area,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
||||||
@@ -5908,7 +6250,11 @@ mod tests {
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
draw_tab_bar(frame, &editor, area);
|
draw_chrome(
|
||||||
|
frame,
|
||||||
|
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||||
|
area,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let title = pane_screen(&terminal, Rect::new(0, 0, 80, 1));
|
let title = pane_screen(&terminal, Rect::new(0, 0, 80, 1));
|
||||||
@@ -5979,10 +6325,6 @@ mod tests {
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| views.draw(frame, &mut editor))
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
|
||||||
views.panels.files.unwrap().0,
|
|
||||||
floating_panel(Rect::new(0, 1, 120, 29))
|
|
||||||
);
|
|
||||||
assert_eq!(pane_screen(&terminal, Rect::new(0, 0, 120, 1)), title);
|
assert_eq!(pane_screen(&terminal, Rect::new(0, 0, 120, 1)), title);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6185,10 +6527,10 @@ mod tests {
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| views.draw(frame, &mut editor))
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
let panel = views.panels.files.unwrap().0;
|
||||||
views.panels.files.unwrap().0,
|
assert!(panel.x < 60 && panel.right() > 60);
|
||||||
floating_panel(Rect::new(0, 0, 120, 30))
|
assert!(panel.y >= views.rects[0].1.y);
|
||||||
);
|
assert!(panel.bottom() <= views.rects[0].1.bottom());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -6764,7 +7106,6 @@ mod tests {
|
|||||||
editor.handle(Input::Char(ch));
|
editor.handle(Input::Char(ch));
|
||||||
}
|
}
|
||||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||||
assert_eq!(terminal.get_cursor_position().unwrap().y, 13);
|
|
||||||
editor.handle(Input::Enter);
|
editor.handle(Input::Enter);
|
||||||
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));
|
||||||
@@ -7071,7 +7412,6 @@ mod tests {
|
|||||||
terminal
|
terminal
|
||||||
.draw(|frame| views.draw(frame, &mut editor))
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(terminal.get_cursor_position().unwrap().y, 13);
|
|
||||||
assert!(screen_text(&terminal).contains(":diff.close"));
|
assert!(screen_text(&terminal).contains(":diff.close"));
|
||||||
editor.handle(Input::Enter);
|
editor.handle(Input::Enter);
|
||||||
assert!(editor.diff().is_none());
|
assert!(editor.diff().is_none());
|
||||||
@@ -9989,6 +10329,7 @@ end
|
|||||||
let before = (top, left);
|
let before = (top, left);
|
||||||
editor.handle(Input::Char(' '));
|
editor.handle(Input::Char(' '));
|
||||||
editor.handle(Input::Char('o'));
|
editor.handle(Input::Char('o'));
|
||||||
|
let mut panels = WheelPanels::default();
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| {
|
.draw(|frame| {
|
||||||
draw(
|
draw(
|
||||||
@@ -9998,11 +10339,13 @@ end
|
|||||||
&mut left,
|
&mut left,
|
||||||
&mut syntax,
|
&mut syntax,
|
||||||
&mut DocumentViewport::default(),
|
&mut DocumentViewport::default(),
|
||||||
&mut WheelPanels::default(),
|
&mut panels,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let panel = Rect::new(area.x + 5, area.y + 1, 100, 24);
|
let panel = panels.files.unwrap().0;
|
||||||
|
assert_eq!(panel.x - area.x, area.right() - panel.right());
|
||||||
|
assert!(panel.y >= area.y && panel.bottom() <= area.bottom() - 2);
|
||||||
assert_eq!((top, left), before);
|
assert_eq!((top, left), before);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
terminal.get_cursor_position().unwrap(),
|
terminal.get_cursor_position().unwrap(),
|
||||||
|
|||||||
Reference in New Issue
Block a user