Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]

### Added
- add Visual Line mode (`V`) to select whole lines
- show the hovered link's URL in the status bar
- add triple-click to select the whole line
- add `--maximized` and `--fullscreen` flags to start the window in that mode
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ visual_bell = true # default: false
| Binding | Action |
|---|---|
| `v` | Enter Visual mode |
| `V` | Enter Visual LINE mode (linewise selection) |
| `i` / `Escape` | Return to Insert mode |
| `j` / `k` | Scroll down / up |
| `/` | Open search |
Expand All @@ -318,13 +319,16 @@ visual_bell = true # default: false

Navigate freely to position the cursor, press `v` to set the selection anchor, then move to the end and copy.

`V` enters Visual LINE mode, where the selection always spans whole lines: `j`/`k` extend it line by line and `y` copies every selected line. `v` returns to characterwise selection. The status bar badge reads `V-LINE`. Triple-clicking a line also leaves you in Visual LINE, so the selection can be extended from there.

| Binding | Action |
|---|---|
| `h/j/k/l` or arrows | Move cursor (scrolls viewport at boundaries) |
| `w` / `b` / `e` | Forward word / backward word / end of word |
| `0` / `$` | Start / end of line |
| `g` / `G` | Top / bottom of viewport |
| `v` | Set selection anchor at cursor (starts highlighting) |
| `v` | Set selection anchor at cursor (starts highlighting; returns to charwise) |
| `V` | Visual LINE: anchor a whole-line selection at the cursor |
| `o` | Swap anchor and cursor |
| `y` / `Ctrl+C` | Copy selection and exit |
| `Y` | Yank (copy) the entire line at cursor |
Expand Down
12 changes: 10 additions & 2 deletions doc/LLMs.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,18 @@ Constants in `src/ui/layout.rs`: `TAB_BAR_H = 22`, `STATUS_BAR_H = 22`.

## Visual Mode Selection (implemented)

- `InputMode::Visual { start_col, start_row, cur_col, cur_row, anchored: bool }`
- `InputMode::Visual { start_col, start_row, cur_col, cur_row, anchored: bool, linewise: bool }`
- `anchored: false` — cursor navigates freely, no selection highlight shown
- `anchored: true` — selection highlighted from `(start_col, start_row)` to `(cur_col, cur_row)`
- `v` in Visual mode → `Action::VisualAnchor` → sets `start = cur, anchored = true`
- `v` in Visual mode → `Action::VisualAnchor` → sets `start = cur, anchored = true, linewise = false`
- `V` (Normal or Visual) → `Action::VisualLineAnchor` → `set_visual_line_anchor()` sets `start_col = 0, start_row = cur_row, anchored = true, linewise = true`
- `V` must NOT travel via `Action::SetMode(Visual{..})`: `do_set_mode` discards keybinding-supplied fields on a Normal→Visual transition, silently dropping `linewise`
- `linewise: true` — the selection spans whole rows; only two computations are linewise-aware:
1. `selection_range` in `src/renderer/text.rs` normalizes to `(0, start_row, grid.cols-1, cur_row)` — nothing downstream changes
2. `do_visual_copy` passes `sc = 0, ec = grid.cols-1` to `grid.selected_text` (its `row_col_range` already makes interior rows full-width)
- Every reconstruction of the `Visual` variant (motions, scroll, boundary scroll, swap anchor) must carry `linewise` through unchanged
- Status bar badge: `V-LINE` when `linewise`, else `VISUAL` (`mode_style` in `src/renderer/draw_fns.rs`)
- Triple-click (`select_line_at` in `src/input/mouse_ops.rs`) enters `linewise: true`, so the picked line can be extended with `j`/`k`
- `o` → `Action::VisualSwapAnchor` → swaps start ↔ cur
- `w`/`b`/`e` → `Action::VisualWordForward/Backward/End` → handled in `main.rs` via `motion::word_*`
- `y` → `Action::Copy`; `Y` → `Action::VisualYankLine` (copies `cur_row`, exits to Insert)
Expand Down
11 changes: 10 additions & 1 deletion doc/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ writing a file.
|---|---|
| `i` / `Escape` | Return to Insert mode |
| `v` | Enter Visual mode |
| `V` | Enter Visual LINE mode |
| `/` | Enter Search mode |
| `n` | Next search match |
| `N` | Previous search match |
Expand All @@ -555,13 +556,21 @@ selection by moving the cursor. `k`/↑ at the top row and `j`/↓ at the bottom
row scroll the viewport, making the entire scrollback buffer reachable. Scroll
actions shift the anchor coordinates so the selected content stays stable.

`V` switches to **Visual LINE**, a linewise variant: the selection always spans
whole rows regardless of the cursor column, `j`/`k` extend it a line at a time,
and `y` copies every selected line (trailing spaces trimmed, rows joined with
`\n`). `v` returns to characterwise selection. The status bar badge reads
`V-LINE`. Triple-click also enters Visual LINE, so a mouse-picked line can be
extended with `j`/`k`.

| Key | Action |
|---|---|
| `h/j/k/l` or arrows | Move cursor (scrolls viewport when at boundary) |
| `w` / `b` / `e` | Start of next word / start of prev word / end of word |
| `0` / `$` | Start / end of line |
| `g` / `G` | Top / bottom of viewport |
| `v` | Set selection anchor at cursor (activates highlight) |
| `v` | Set selection anchor at cursor (activates highlight; charwise) |
| `V` | Visual LINE: anchor a whole-line selection at the cursor |
| `o` | Swap anchor and cursor (extend from either end) |
| `y` / `Ctrl+C` | Copy selection to clipboard, return to Insert mode |
| `Y` | Yank (copy) the entire line at the cursor, return to Insert mode |
Expand Down
49 changes: 42 additions & 7 deletions src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ impl AppState {
cur_col,
cur_row,
anchored,
linewise,
} = self.mode().clone()
else {
return;
Expand All @@ -356,6 +357,7 @@ impl AppState {
cur_col: nc,
cur_row: nr,
anchored,
linewise,
};
}

Expand All @@ -382,6 +384,7 @@ impl AppState {
cur_col,
cur_row,
anchored,
linewise,
} = self.mode().clone()
{
self.tab_mut().mode = InputMode::Visual {
Expand All @@ -390,6 +393,7 @@ impl AppState {
cur_col,
cur_row: (cur_row + n).min(grid_rows.saturating_sub(1)),
anchored,
linewise,
};
}
}
Expand All @@ -401,6 +405,7 @@ impl AppState {
cur_col,
cur_row,
anchored,
linewise,
} = self.mode().clone()
{
self.tab_mut().mode = InputMode::Visual {
Expand All @@ -409,6 +414,7 @@ impl AppState {
cur_col,
cur_row: cur_row.saturating_sub(n),
anchored,
linewise,
};
}
}
Expand Down Expand Up @@ -439,17 +445,17 @@ impl AppState {
cur_col,
cur_row,
anchored: true,
linewise,
} = self.mode().clone()
{
let text = self.active_entry().and_then(|entry| {
entry.pane.grid_read().map(|g| {
g.selected_text(
start_col,
start_row,
cur_col,
cur_row,
entry.pane.scroll_offset,
)
let (sc, ec) = if linewise {
(0, g.cols.saturating_sub(1))
} else {
(start_col, cur_col)
};
g.selected_text(sc, start_row, ec, cur_row, entry.pane.scroll_offset)
})
});
if let Some(text) = text {
Expand Down Expand Up @@ -481,6 +487,7 @@ impl AppState {
cur_col,
cur_row,
anchored,
linewise,
} = self.mode().clone()
{
let grid_rows = self.active_grid_rows();
Expand All @@ -490,6 +497,7 @@ impl AppState {
cur_col: start_col,
cur_row: start_row.min(grid_rows.saturating_sub(1)),
anchored,
linewise,
};
}
}
Expand All @@ -505,15 +513,36 @@ impl AppState {
cur_col,
cur_row,
anchored: true,
linewise: false,
};
}
}

/// `V` — enter (or switch to) Visual LINE: anchor a whole-row selection at
/// the current row.
fn set_visual_line_anchor(&mut self) {
let (cur_col, row) = match self.mode().clone() {
InputMode::Visual {
cur_col, cur_row, ..
} => (cur_col, cur_row),
_ => self.visual_start_pos(),
};
self.tab_mut().mode = InputMode::Visual {
start_col: 0,
start_row: row,
cur_col,
cur_row: row,
anchored: true,
linewise: true,
};
}

fn dispatch_visual_action(&mut self, action: Action) -> Vec<AppEffect> {
match action {
Action::Copy => self.do_visual_copy(),
Action::VisualSwapAnchor => self.swap_visual_anchor(),
Action::VisualAnchor => self.set_visual_anchor(),
Action::VisualLineAnchor => self.set_visual_line_anchor(),
Action::VisualWordForward => {
self.move_visual_cursor(crate::input::motion::word_forward)
}
Expand All @@ -536,6 +565,7 @@ impl AppState {
start_row,
cur_col,
anchored,
linewise,
..
} = self.mode().clone()
{
Expand All @@ -545,6 +575,7 @@ impl AppState {
cur_col,
cur_row: 0,
anchored,
linewise,
};
}
}
Expand All @@ -559,6 +590,7 @@ impl AppState {
start_row,
cur_col,
anchored,
linewise,
..
} = self.mode().clone()
{
Expand All @@ -568,6 +600,7 @@ impl AppState {
cur_col,
cur_row: grid_rows.saturating_sub(1),
anchored,
linewise,
};
}
}
Expand Down Expand Up @@ -688,6 +721,7 @@ impl AppState {
Action::Copy
| Action::VisualSwapAnchor
| Action::VisualAnchor
| Action::VisualLineAnchor
| Action::VisualWordForward
| Action::VisualWordBackward
| Action::VisualWordEnd
Expand Down Expand Up @@ -858,6 +892,7 @@ impl AppState {
cur_col: col,
cur_row: row,
anchored: false,
linewise: false,
}
}
} else {
Expand Down
Loading