Skip to content

Lots of small fixes and improvements#23

Merged
JackDerksen merged 18 commits intomainfrom
small-things
Apr 22, 2026
Merged

Lots of small fixes and improvements#23
JackDerksen merged 18 commits intomainfrom
small-things

Conversation

@JackDerksen
Copy link
Copy Markdown
Owner

@JackDerksen JackDerksen commented Apr 21, 2026

Lots of small miscellaneous fixes and improvements here:

  • Prevent the blank/empty buffer from leaking into the buffer list for commands like :bn and :ls.
  • Add ~ motion to invert uppercase/lowercase letters
  • Add F (shift+f) and T (shift+t) motions for backwards search
  • Add % motion for jumping to the opening/closing delimiter match of the one under the cursor
  • Fix scrolloff behaviour at the right edge of the command popup
  • Add session-bound command history (accessible with arrow keys or ctrl+n/ctrl+p)
  • Change the notification system from statusline messages to small toasts in the top right corner of the viewport
  • Automatically trim trailing whitespace when saving a file
  • Fix the cursor being hidden when the :perf popup is active

Summary by CodeRabbit

Release Notes

  • New Features

    • Toggle character case with ~ in Normal and Visual modes
    • Jump to matching delimiters with %
    • Backward character find motions: F and T
    • Command history navigation with up/down keys in command mode
    • Status messages now expire after 5 seconds
    • Search displays match count in status message
    • Status messages appear as toast notifications
    • Trailing whitespace trimmed automatically on file save
  • Documentation

    • Updated README with new motion documentation
  • Tests

    • Added comprehensive tests for new motions and features

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@JackDerksen has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 44 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 8 minutes and 44 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 682b6905-1b11-425e-a207-b579934926df

📥 Commits

Reviewing files that changed from the base of the PR and between f28c547 and 0a0bae1.

📒 Files selected for processing (11)
  • crates/redox-core/src/buffer/text_buffer/search.rs
  • crates/redox-tui/src/app/state.rs
  • crates/redox-tui/src/app/state/commands.rs
  • crates/redox-tui/src/app/state/editing.rs
  • crates/redox-tui/src/app/state/search.rs
  • crates/redox-tui/src/app/state/tests.rs
  • crates/redox-tui/src/lib.rs
  • crates/redox-tui/src/ui/mod.rs
  • crates/redox-tui/src/ui/widgets/command_line.rs
  • crates/redox-tui/src/ui/widgets/mod.rs
  • crates/redox-tui/src/ui/widgets/toast.rs
📝 Walkthrough

Walkthrough

This PR adds four Vim motions (~, %, F, T) with supporting core functionality and UI features. It implements trailing whitespace trimming on save, time-based status message expiry, command history navigation, a status toast widget for search feedback, and case-toggling in normal and visual modes across the editor.

Changes

Cohort / File(s) Summary
Documentation
README.md
Updated Vim motions table with new entries: ~ (toggle case), % (matching delimiter), F/T (backward find/till motions).
Core Text Buffer Search
crates/redox-core/src/buffer/text_buffer/search.rs
Added find_char_before_on_line and matching_delimiter public methods with internal delimiter-pairing logic supporting asymmetric pairs ()[]{} <> and symmetric delimiters, plus escape-detection for proper pairing.
Core Motion System
crates/redox-core/src/motion.rs
Extended Motion enum with FindCharBefore, TillCharBefore, and MatchDelimiter variants; updated apply_motion family to handle backward search and delimiter matching with count iteration and boundary clamping.
Core Text Editing
crates/redox-core/src/buffer/text_buffer/editing.rs
Added trim_trailing_whitespace method for removing trailing spaces/tabs from buffer lines.
TUI State Management
crates/redox-tui/src/app/state.rs
Replaced boolean status-clear flag with time-based expiry (status_msg_expires_at: Option<Instant>); added CommandHistoryState struct and history navigation support.
TUI Input Handling
crates/redox-tui/src/input/mod.rs
Added InputAction variants: ToggleCase { count }, CommandHistoryPrev, CommandHistoryNext; mapped ~, %, F, T to new motions and actions.
TUI Action Processing
crates/redox-tui/src/app/state/actions.rs
Updated input action dispatch to handle new motions, command history navigation, case toggling, and char-search motion detection; removed unconditional status-message clearing.
TUI Command Execution
crates/redox-tui/src/app/state/commands.rs
Implemented command history push/navigation with duplicate suppression; added trailing-whitespace trimming before save with view reconciliation; introduced helper methods for history state management.
TUI Editor Operations
crates/redox-tui/src/app/state/editing.rs
Implemented toggle_case_under_cursor_or_selection for normal and visual modes; added delimiter-operator support (MatchDelimiter); introduced case-transformation helpers.
TUI Search
crates/redox-tui/src/app/state/search.rs
Extended backward-motion search support; added search match count reporting via format_search_match_count status messages.
TUI Surface/Buffer Management
crates/redox-tui/src/app/state/surface.rs, crates/redox-tui/src/app/state/explorer.rs
Added close_inactive_empty_unnamed_startup_buffer method; integrated placeholder buffer cleanup when opening files from explorer.
TUI Status Bar & Toast Widget
crates/redox-tui/src/ui/widgets/status_bar.rs, crates/redox-tui/src/ui/widgets/toast.rs, crates/redox-tui/src/ui/widgets/mod.rs
Refactored status bar to prioritize explorer mode display; added new draw_status_toast widget with perf-popup-aware placement logic and text wrapping/truncation.
TUI Performance Popup Helpers
crates/redox-tui/src/ui/widgets/perf.rs
Extracted popup layout computation into reusable PerfPopupLayout struct and perf_popup_layout/perf_popup_occludes_cursor public functions.
TUI Command Line
crates/redox-tui/src/ui/widgets/command_line.rs
Improved cursor positioning via new command_line_view helper; added right-viewport clipping for long inputs with grapheme-aware trimming.
TUI Integration & Exports
crates/redox-tui/src/lib.rs, crates/redox-tui/src/ui/mod.rs
Integrated status toast rendering into main draw path; added per-frame status-message expiry; updated re-exports for new UI helpers.
Test Coverage
crates/redox-tui/src/app/state/tests.rs
Added extensive tests for command history, delimiter motions, backward find/till, case toggling across modes, search feedback, trailing-whitespace trimming, and placeholder-buffer cleanup.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant InputHandler as Input Handler
    participant Motions as Motion Resolver
    participant Buffer as TextBuffer
    participant UI as UI Renderer

    rect rgb(100, 150, 200, 0.5)
    Note over User,Buffer: Delimiter Matching (%)
    User->>InputHandler: Press % over delimiter
    InputHandler->>Motions: MatchDelimiter motion
    Motions->>Buffer: matching_delimiter(cursor_pos)
    Buffer->>Buffer: Scan for paired delimiter
    Buffer-->>Motions: Matching position
    Motions-->>UI: Cursor moves to match
    UI-->>User: Jump to paired bracket
    end

    rect rgb(100, 200, 150, 0.5)
    Note over User,UI: Backward Character Find (F/T)
    User->>InputHandler: Press F, then char
    InputHandler->>Motions: FindCharBefore(char)
    Motions->>Buffer: find_char_before_on_line()
    Buffer-->>Motions: Previous position
    Motions-->>UI: Cursor repositioned
    UI-->>User: Jump to last occurrence
    end

    rect rgb(200, 150, 100, 0.5)
    Note over User,UI: Case Toggle (~)
    User->>InputHandler: Press ~ with count
    InputHandler->>InputHandler: ToggleCase action
    InputHandler->>Buffer: toggle_case_under_cursor_or_selection()
    Buffer->>Buffer: Replace chars (lower↔upper)
    Buffer-->>InputHandler: Update buffer
    InputHandler-->>UI: Render changes
    UI-->>User: Characters toggled
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hops through delimiters with a % bound,
F and T hunt backwards on solid ground,
~ flips the case with fuzzy delight,
While toasts pop and fade—quick, ephemeral light!
History remembers each keystroke's dance, 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Lots of small fixes and improvements' is too vague and generic to meaningfully convey the scope of changes, which includes significant features like motion additions, command history, status toasts, and buffer management. Consider using a more specific title that highlights the primary changes, such as 'Add vim motions (~, F, T, %) and improve UI feedback with status toasts' or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch small-things

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/redox-tui/src/app/state/commands.rs (1)

228-265: ⚠️ Potential issue | 🟡 Minor

Trim-on-save leaves dirty bookkeeping stale if the write fails.

The trim branch records the undo and invalidates caches, but never calls recompute_active_dirty(). On the happy path save_active() clears dirty, so nothing is visibly wrong. But if save_active() returns Err, the buffer has been mutated (whitespace stripped) while the dirty flag still reflects the pre-trim state. For a buffer that was clean before :w (e.g. disk changed underneath, or a read-only mount causing the write to fail), the user sees trimmed content with no dirty indicator until the next edit.

🛠 Suggested fix
         let before = self.capture_active_undo_snapshot();
         let trimmed = self.trim_active_trailing_whitespace();
         if trimmed {
             let (viewport_width_cells, viewport_height_rows) = self.viewport_size();
             let text_vh = viewport_height_rows.saturating_sub(STATUS_BAR_HEIGHT_ROWS);
             let active_id = self.session.active_id();
             let view = self.views.entry(active_id).or_default();
             let buffer = self.session.active_buffer();
             view.cursor
                 .reconcile_after_edit(buffer, viewport_width_cells, text_vh);
             self.invalidate_active_render_caches();
             let _ = self.record_active_undo_if_changed(before);
+            let _ = self.session.recompute_active_dirty();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/redox-tui/src/app/state/commands.rs` around lines 228 - 265, The
trim-on-save branch mutates the buffer (via trim_active_trailing_whitespace) and
records an undo but never updates dirty bookkeeping; ensure the buffer's dirty
state is recomputed immediately after the mutation/undo-recording so it reflects
the trimmed content even if save_active() fails. In write_current_file, after
calling self.record_active_undo_if_changed(before) (inside the trimmed branch)
call self.recompute_active_dirty() (or equivalent) so recompute_active_dirty()
runs before attempting self.session.save_active(); this guarantees the dirty
flag is correct whether save succeeds or errors.
🧹 Nitpick comments (1)
crates/redox-tui/src/app/state/commands.rs (1)

139-149: Unbounded command history growth.

push_command_history only dedupes consecutive duplicates — there's no cap on command_history.entries. For a session-bound history this is unlikely to bite users in practice, but it's a trivial upgrade to bound it (e.g. keep the last N, say 200) and it also avoids pathological growth if the same alternating two commands are run thousands of times.

♻️ Sketch
 fn push_command_history(&mut self, command: String) {
     if self
         .command_history
         .entries
         .last()
         .is_some_and(|previous| previous == &command)
     {
         return;
     }
     self.command_history.entries.push(command);
+    const MAX_COMMAND_HISTORY: usize = 200;
+    if self.command_history.entries.len() > MAX_COMMAND_HISTORY {
+        let overflow = self.command_history.entries.len() - MAX_COMMAND_HISTORY;
+        self.command_history.entries.drain(0..overflow);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/redox-tui/src/app/state/commands.rs` around lines 139 - 149,
push_command_history currently only dedupes consecutive duplicates and lets
command_history.entries grow unbounded; add a fixed cap (e.g. const MAX_HISTORY:
usize = 200) and after pushing the new command trim the Vec to keep only the
most recent MAX_HISTORY entries. Keep the existing consecutive-duplicate check
in push_command_history, push the command as now, then if
self.command_history.entries.len() > MAX_HISTORY remove the oldest entries (e.g.
drain(0..excess) where excess = len - MAX_HISTORY) so the history never exceeds
the cap.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/redox-core/src/buffer/text_buffer/search.rs`:
- Around line 46-146: The asymmetric delimiters are not skipping escaped
characters, so escaped opens/closes (e.g., '\(') are treated as structural
matches; update matching_delimiter to consult char_is_escaped_for_pairing for
the current char (use the computed char_idx) and return None when the delimiter
is escaped before dispatching to match_asymmetric_delimiter_forward or
match_asymmetric_delimiter_backward; alternatively add the same escaped check at
the start of match_asymmetric_delimiter_forward and
match_asymmetric_delimiter_backward (using char_idx/end_idx) so escaped
asymmetric delimiters are ignored the same way symmetric ones are.

In `@crates/redox-tui/src/app/state/editing.rs`:
- Around line 495-542: The loop in toggle_case_under_cursor can encounter
multi-character expansions from toggled_case_text (via
char::to_uppercase()/to_lowercase()), which causes the cursor to advance by the
replacement length and skip over the expanded text; this is intentional but
should be documented and covered by tests—add an inline comment in
toggle_case_under_cursor explaining that multi-codepoint replacements (e.g.,
'ß'→"SS") will advance cursor past the expansion and that this matches Vim-like
behavior, and add/plan a unit/integration test targeting toggled_case_text +
replace_selection behavior to assert correct cursor advancement and rendering
after replacements.

In `@crates/redox-tui/src/app/state/search.rs`:
- Around line 276-279: The match arm that constructs a SearchQuery for
Motion::FindCharBefore and Motion::TillCharBefore should preserve the original
`T` semantics by using SearchLanding::AfterMatch for TillCharBefore; update the
pattern so Motion::FindCharBefore continues to map to SearchLanding::OnMatch
while Motion::TillCharBefore maps to SearchLanding::AfterMatch when building the
SearchQuery (refer to SearchQuery and SearchLanding in the same match).

In `@crates/redox-tui/src/lib.rs`:
- Around line 320-329: The status toast's layout isn't considered when deciding
whether to hide the cursor: after calling draw_status_toast(state, style,
window) the code only checks perf_popup_layout via perf_popup_occludes_cursor,
so a cursor under the toast can remain visible. Change draw_status_toast to
expose/return the toast layout (or provide a way to query it), then in the same
cursor handling block use that layout together with perf_popup_layout and
perf_popup_occludes_cursor to decide whether to call hide_cursor(window) or
window.request_cursor(cursor); update references to draw_status_toast,
perf_popup_layout, perf_popup_occludes_cursor, hide_cursor,
window.request_cursor and cursor_spec accordingly.

In `@crates/redox-tui/src/ui/widgets/command_line.rs`:
- Around line 95-103: The branch treats exact-fit inputs as clipped; change the
clip boundary so text_width == input_width is handled by the unclipped path: in
command_line.rs update the condition that currently checks
command_text_width(text) < input_width to use <= instead, so when
command_text_width(text) <= input_width you return (clip_text_to_cells(text,
input_width), command_text_width(&clipped)) and only call
clip_text_right_to_cells(text, visible_width) when text is strictly wider; the
referenced symbols are command_text_width, clip_text_to_cells,
clip_text_right_to_cells, text/input_width.

In `@crates/redox-tui/src/ui/widgets/toast.rs`:
- Around line 29-31: Only apply the toast offset when the perf popup is actually
going to be drawn: change the logic that computes perf_popup_layout so it checks
the real draw condition used in lib.rs (i.e. ensure the same mode/visibility
check that causes the perf popup to be skipped in command/search modes) instead
of unconditionally calling state.perf_popup(). Locate the use of
state.perf_popup() and perf_popup_layout in toast rendering (symbols:
state.perf_popup, perf_popup_layout, the toast positioning code) and guard the
layout/offset code with the same visibility/mode check so toasts are not shifted
by a non-drawn (phantom) popup.

---

Outside diff comments:
In `@crates/redox-tui/src/app/state/commands.rs`:
- Around line 228-265: The trim-on-save branch mutates the buffer (via
trim_active_trailing_whitespace) and records an undo but never updates dirty
bookkeeping; ensure the buffer's dirty state is recomputed immediately after the
mutation/undo-recording so it reflects the trimmed content even if save_active()
fails. In write_current_file, after calling
self.record_active_undo_if_changed(before) (inside the trimmed branch) call
self.recompute_active_dirty() (or equivalent) so recompute_active_dirty() runs
before attempting self.session.save_active(); this guarantees the dirty flag is
correct whether save succeeds or errors.

---

Nitpick comments:
In `@crates/redox-tui/src/app/state/commands.rs`:
- Around line 139-149: push_command_history currently only dedupes consecutive
duplicates and lets command_history.entries grow unbounded; add a fixed cap
(e.g. const MAX_HISTORY: usize = 200) and after pushing the new command trim the
Vec to keep only the most recent MAX_HISTORY entries. Keep the existing
consecutive-duplicate check in push_command_history, push the command as now,
then if self.command_history.entries.len() > MAX_HISTORY remove the oldest
entries (e.g. drain(0..excess) where excess = len - MAX_HISTORY) so the history
never exceeds the cap.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97945a04-bb74-4da1-9e76-ea8b88a68241

📥 Commits

Reviewing files that changed from the base of the PR and between d0cfe8f and f28c547.

📒 Files selected for processing (20)
  • README.md
  • crates/redox-core/src/buffer/text_buffer/editing.rs
  • crates/redox-core/src/buffer/text_buffer/search.rs
  • crates/redox-core/src/motion.rs
  • crates/redox-tui/src/app/state.rs
  • crates/redox-tui/src/app/state/actions.rs
  • crates/redox-tui/src/app/state/commands.rs
  • crates/redox-tui/src/app/state/editing.rs
  • crates/redox-tui/src/app/state/explorer.rs
  • crates/redox-tui/src/app/state/search.rs
  • crates/redox-tui/src/app/state/surface.rs
  • crates/redox-tui/src/app/state/tests.rs
  • crates/redox-tui/src/input/mod.rs
  • crates/redox-tui/src/lib.rs
  • crates/redox-tui/src/ui/mod.rs
  • crates/redox-tui/src/ui/widgets/command_line.rs
  • crates/redox-tui/src/ui/widgets/mod.rs
  • crates/redox-tui/src/ui/widgets/perf.rs
  • crates/redox-tui/src/ui/widgets/status_bar.rs
  • crates/redox-tui/src/ui/widgets/toast.rs

Comment thread crates/redox-core/src/buffer/text_buffer/search.rs
Comment thread crates/redox-tui/src/app/state/editing.rs
Comment thread crates/redox-tui/src/app/state/search.rs Outdated
Comment thread crates/redox-tui/src/lib.rs Outdated
Comment thread crates/redox-tui/src/ui/widgets/command_line.rs
Comment thread crates/redox-tui/src/ui/widgets/toast.rs Outdated
@JackDerksen JackDerksen merged commit 724a8ae into main Apr 22, 2026
2 checks passed
@JackDerksen JackDerksen deleted the small-things branch April 22, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant