feat: bind Ctrl-U to backward line deletion
This commit is contained in:
@@ -126,9 +126,9 @@ Key notation follows [Herdr's readable style](https://herdr.dev/docs/configurati
|
||||
|
||||
Normal mode binds `ctrl+o` to `config.core.jump_back` and `ctrl+i`/`tab` to `config.core.jump_forward`. Insert-mode Tab inserts literal text outside snippets; inside one it moves to the next argument. Override or disable these keys with the ordinary per-mode binding interface; jump actions also appear in the command palette.
|
||||
|
||||
`config.core.backspace_word` and `config.core.backspace_line` are typed actions eligible in Insert, Command, and Search. Any platform can explicitly bind them to supported keys; only macOS defaults bind Option+Backspace and Command+Backspace respectively. For example, `config.bind "ctrl+w", config.core.backspace_word, mode: config.modes.insert`. Word deletion follows Unicode word boundaries after skipping trailing whitespace; line deletion removes the caret-to-logical-line-start prefix, not the suffix or newline. Active snippets confine both actions to their argument. macOS prompt and filter queries also recognize these modified Backspace keys.
|
||||
`config.core.backspace_word` and `config.core.backspace_line` are typed actions eligible in Insert, Command, and Search. Ctrl+u defaults to `config.core.backspace_line` in all three modes on every platform; it can be rebound or disabled with the ordinary per-mode binding interface. macOS additionally defaults Option+Backspace and Command+Backspace to word and line deletion respectively. Any platform can explicitly bind these actions to supported keys; for example, `config.bind "ctrl+w", config.core.backspace_word, mode: config.modes.insert`. Word deletion follows Unicode word boundaries after skipping trailing whitespace; line deletion removes the caret-to-logical-line-start prefix, not the suffix or newline. Active snippets confine both actions to their argument. macOS prompt and filter queries also recognize these modified Backspace keys.
|
||||
|
||||
Configuration accepts the Option/Alt and Command/Super Backspace names on every platform, but terminal modifier-event translation for these keys is macOS-only. The terminal must deliver Option or Super (Command in Kitty CSI-u); Meta is not Command. Some terminals intercept Command+Backspace or encode it as Ctrl+u instead, which does not invoke the default line-deletion action. Legacy ESC+DEL supports Option+Backspace without keyboard protocol negotiation.
|
||||
Configuration accepts the Option/Alt and Command/Super Backspace names on every platform, but terminal modifier-event translation for these keys is macOS-only. Native modifier events must deliver Option or Super (Command in Kitty CSI-u); Meta is not Command. Ghostty's default Command+Backspace encoding is Ctrl+u (`\x15`), which invokes the same line-deletion action without a Ghostty override. It remains a distinct Ctrl+u binding, not a reconstructed Command modifier. Legacy ESC+DEL supports Option+Backspace without keyboard protocol negotiation.
|
||||
|
||||
Normal mode uses literal `_` and `|` for above/below and left/right splits, and `alt+h/j/k/l` for directional pane focus. Bind the produced punctuation rather than a physical Shift combination. Close actions have no default binding; for example, `config.bind "leader x", config.core.pane.close`. See [panes and splits](editing.md#panes-and-splits) for shared-buffer and unsaved-change policies.
|
||||
|
||||
|
||||
+4
-3
@@ -38,15 +38,16 @@ Default controls and editing behavior. For custom bindings, see [configuration](
|
||||
| Insert, active snippet | Type to replace the highlighted argument and its mirrors; Tab / Shift+Tab moves next/previous |
|
||||
| Command | Enter executes; Backspace edits; Esc cancels |
|
||||
| Search | Type a literal query; Enter finds the next match; Backspace edits; Esc cancels |
|
||||
| Insert, Command, Search | Ctrl+u deletes backward to the logical line start |
|
||||
| Insert, Command, Search (macOS) | Option+Backspace deletes backward by word; Command+Backspace deletes backward to the logical line start |
|
||||
|
||||
`J` removes the next line's leading spaces/tabs and preserves existing trailing whitespace. It normally adds one space, two after `.`, `?`, or `!` (Vim's default), and none before `)` or when either side is empty. The cursor lands at the join point; the join is one undo action. On the last line, `J` does nothing.
|
||||
|
||||
`gt`/`gb` move every caret to the first nonblank grapheme on the first/last row, including the empty final row after a trailing newline. Duplicate carets and overlapping Visual selections normalize as usual. Both Visual shapes retain their anchors and extend to the destination. These motions do not edit text or add undo history; count prefixes are consumed rather than interpreted as line numbers. `g` waits for its second key, and Escape cancels only the pending prefix. In Insert and command/search prompts, `gt`/`gb` remain literal text.
|
||||
|
||||
On macOS, these modified Backspace defaults also edit prompt and filter queries, including file lists, the command palette, and revision selectors. Option+Backspace uses Unicode word boundaries, first skipping trailing whitespace; Command+Backspace removes only the prefix before the caret on the current logical line, preserving its suffix and newline. Buffer deletion runs at every caret, merges overlapping ranges, shares the Insert undo group, and dot repeats the boundary operation rather than the old deleted length. Within an active snippet, both operations stay confined to the current argument and update its mirrors. Other platforms retain their existing defaults.
|
||||
On macOS, these modified Backspace defaults also edit prompt and filter queries, including file lists, the command palette, and revision selectors. Option+Backspace uses Unicode word boundaries, first skipping trailing whitespace; Command+Backspace removes only the prefix before the caret on the current logical line, preserving its suffix and newline. Buffer deletion runs at every caret, merges overlapping ranges, shares the Insert undo group, and dot repeats the boundary operation rather than the old deleted length. Within an active snippet, both operations stay confined to the current argument and update its mirrors. These modified Backspace defaults remain macOS-only; Ctrl+u invokes the same line-deletion action on every platform.
|
||||
|
||||
The terminal must deliver the modifier: Option may use legacy ESC+DEL, and Kitty CSI-u represents Command as **Super**, not Meta. Some terminals intercept Command+Backspace or encode it as Ctrl+u; that is not the same key and does not invoke this default line deletion. Vamp does not negotiate an enhanced keyboard protocol or support generic Command-modified character keys.
|
||||
The terminal may deliver a native modifier or a conventional control sequence: Option may use legacy ESC+DEL, and Kitty CSI-u represents Command as **Super**, not Meta. Ghostty defaults to encoding Command+Backspace as Ctrl+u (`\x15`), which invokes the same backward line deletion without a Ghostty configuration override. Ctrl+u remains a separate, configurable binding; vamp cannot distinguish it from Command+Backspace when both send the same bytes. Vamp does not negotiate an enhanced keyboard protocol or support generic Command-modified character keys.
|
||||
|
||||
## Manual LSP completion
|
||||
|
||||
@@ -361,7 +362,7 @@ Binary data, missing final newlines, permission/type changes, symlink targets, a
|
||||
|
||||
Ctrl+p opens a searchable overlay without leaving the current mode. It lists each core action once, plus registered plugin commands, with their owner, effective bindings in the current mode, and eligible modes. Disabled or overridden bindings are not advertised; actions with no key remain available through the palette. Mode-ineligible entries are dimmed and cannot execute. Retired plugin commands disappear.
|
||||
|
||||
Search is case-insensitive: every whitespace-separated query token must occur in the action/command name, canonical Ex name, a shorthand alias, human-readable label, or owner. Aliases find the same action rather than creating duplicate entries. Results have a stable label/name order. Ctrl+j/Down/Ctrl+n select the next result; Ctrl+k/Up select the previous, with wraparound. Backspace removes a whole grapheme. Enter with no match or an ineligible selection leaves the palette open. Bracketed paste is ignored while the palette is open.
|
||||
Search is case-insensitive: every whitespace-separated query token must occur in the action/command name, canonical Ex name, a shorthand alias, human-readable label, or owner. Aliases find the same action rather than creating duplicate entries. Results have a stable label/name order. Ctrl+j/Down/Ctrl+n select the next result; Ctrl+k/Up select the previous, with wraparound. Backspace removes a whole grapheme; Ctrl+u clears the query without editing the underlying buffer. Enter with no match or an ineligible selection leaves the palette open. Bracketed paste is ignored while the palette is open.
|
||||
|
||||
Accepting a core action executes it in the underlying mode, including Visual edits and Insert-mode actions. `core.edit` and `core.buffer` instead open prefilled Ex prompts for their required arguments. The destructive `core.force_quit`, `core.buffer_force_delete`, and `core.pane.force_close` actions also open canonical Ex prompts, requiring another Enter before discarding changes. Accepting a plugin command is available only in Normal mode and opens a prefilled `:` prompt; add arguments if needed, then Enter executes through the usual session dispatch.
|
||||
|
||||
|
||||
+60
-2
@@ -1139,6 +1139,7 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
|
||||
for (key, action) in [
|
||||
(Input::Escape, Normal),
|
||||
(Input::Backspace, Backspace),
|
||||
(Input::Ctrl('u'), BackspaceLine),
|
||||
(
|
||||
Input::Enter,
|
||||
if mode == Mode::Insert {
|
||||
@@ -5032,6 +5033,11 @@ impl Editor {
|
||||
self.palette = None;
|
||||
self.pane_close_picker_origin = None;
|
||||
}
|
||||
Input::Ctrl('u') => {
|
||||
if BackspaceKind::Line.truncate(&mut palette.query) {
|
||||
self.refresh_palette();
|
||||
}
|
||||
}
|
||||
Input::Enter => {
|
||||
let entry = palette
|
||||
.entries
|
||||
@@ -15002,7 +15008,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_clear_respects_navigation_bindings_and_does_not_clear_palette_or_idle_dock() {
|
||||
fn filter_clear_respects_navigation_bindings_and_preserves_idle_dock_queries() {
|
||||
let directory = Directory::new();
|
||||
for name in ["a.txt", "b.txt"] {
|
||||
fs::write(directory.path(name), "text").unwrap();
|
||||
@@ -15045,7 +15051,7 @@ mod tests {
|
||||
editor.handle(Input::Ctrl('p'));
|
||||
type_text(&mut editor, "core");
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
assert_eq!(editor.palette().unwrap().query, "core");
|
||||
assert!(editor.palette().unwrap().query.is_empty());
|
||||
assert_eq!(editor.file_picker().unwrap().query, "txt");
|
||||
}
|
||||
}
|
||||
@@ -16245,6 +16251,14 @@ mod tests {
|
||||
let revision = editor.text_revision();
|
||||
let carets = editor.carets();
|
||||
editor.handle(opening);
|
||||
type_text(&mut editor, "discard this query");
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
assert_eq!(
|
||||
editor
|
||||
.palette()
|
||||
.map_or(editor.command_line(), |palette| palette.query.as_str()),
|
||||
""
|
||||
);
|
||||
type_text(&mut editor, "alpha βeta ");
|
||||
editor.handle(Input::AltBackspace);
|
||||
assert_eq!(
|
||||
@@ -16266,6 +16280,50 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_u_deletes_line_prefixes_and_honors_disabled_and_remapped_bindings() {
|
||||
let mut editor = editor();
|
||||
let original = "first\nalpha βeta tail\nlast";
|
||||
editor
|
||||
.apply(vec![Effect::Replace(original.into())])
|
||||
.unwrap();
|
||||
set_carets(&mut editor, &[(1, 11)], 0);
|
||||
editor.handle(Input::Char('i'));
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
assert_eq!(editor.context().text, "first\ntail\nlast");
|
||||
assert_eq!(editor.cursor(), Cursor { row: 1, column: 0 });
|
||||
assert_eq!(editor.mode(), Mode::Insert);
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, original);
|
||||
|
||||
editor.bind("insert", "ctrl+u", None).unwrap();
|
||||
editor.handle(Input::Char('i'));
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
assert_eq!(editor.context().text, original);
|
||||
editor
|
||||
.bind("insert", "ctrl+u", Some("core.backspace_word".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
assert_eq!(editor.context().text, "first\nalpha tail\nlast");
|
||||
|
||||
editor.handle(Input::Escape);
|
||||
for (opening, mode) in [(':', "command"), ('/', "search")] {
|
||||
editor.bind(mode, "ctrl+u", None).unwrap();
|
||||
editor.handle(Input::Char(opening));
|
||||
type_text(&mut editor, "alpha beta");
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
assert_eq!(editor.command_line(), "alpha beta");
|
||||
editor
|
||||
.bind(mode, "ctrl+u", Some("core.backspace_word".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
assert_eq!(editor.command_line(), "alpha ");
|
||||
assert_eq!(editor.context().text, "first\nalpha tail\nlast");
|
||||
editor.handle(Input::Escape);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_leaf_search_opens_direct_match_and_directory_search_enters_browse() {
|
||||
let directory = Directory::new();
|
||||
|
||||
@@ -38,6 +38,7 @@ Insert: Ctrl+n requests LSP completion at primary caret, then cycles next; Ctrl+
|
||||
Candidates replace text live; Original text restores it; other keys close and act normally
|
||||
Snippets expand fully: type replaces argument, Tab/Shift+Tab next/previous, final $0 ends
|
||||
Outside snippets Enter inserts newline, Tab inserts tab; Esc keeps text and returns to Normal
|
||||
Insert/Command/Search: Ctrl+u deletes backward to the logical line start
|
||||
Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps)
|
||||
Commands: :save [PATH], :quit, :force_quit, :save_and_quit [PATH], :plugins, :PLUGIN [ARGS]
|
||||
Aliases: w=save, q=quit, q!=force_quit, wq=save_and_quit
|
||||
|
||||
Reference in New Issue
Block a user