feat: Ctrl+U / Ctrl+K kill to line start / end
cfcd5282e4cab95208e6e04ed95863c16ffe6f3e
humdrum <me@humdrum.me> · 2026-06-28 17:32
parent b029710a
feat: Ctrl+U / Ctrl+K kill to line start / end Ctrl+U deletes from the start of the line to the cursor (matches the macOS Cmd+Delete behavior — map Cmd+Delete→Ctrl+U in the terminal); Ctrl+K deletes to the end. Both stay on the logical line, set dirty, and refresh goal+scroll.
3 files changed
README.md +1 −0
@@ -38,6 +38,7 @@
| Key | Action |
| --- | --- |
| type / arrows / `Enter` / `Backspace` / `Del` | edit and move (Up/Down move by visual line) |
+| `Ctrl+U` / `Ctrl+K` | delete to start / end of line (map `Cmd+Delete`→`Ctrl+U` in your terminal) |
| `Ctrl+S` | save (an unnamed buffer prompts for a name → inbox) |
| `Ctrl+P` | toggle the Glamour read preview |
| `Ctrl+F` | fuzzy file picker (with live preview) |
internal/editor/editor.go +29 −0
@@ -71,6 +71,31 @@
func (e *Editor) curLine() []rune { return []rune(e.Lines[e.Cursor.Row]) }
func (e *Editor) setLine(rs []rune) { e.Lines[e.Cursor.Row] = string(rs) }
+// KillToLineStart deletes from the start of the line to the cursor and parks the
+// cursor at column 0 (Ctrl+U; the macOS Cmd+Delete behavior).
+func (e *Editor) KillToLineStart() {
+ if e.Cursor.Col == 0 {
+ return
+ }
+ e.setLine(e.curLine()[e.Cursor.Col:])
+ e.Cursor.Col = 0
+ e.Dirty = true
+ e.setGoal()
+ e.followCursor()
+}
+
+// KillToLineEnd deletes from the cursor to the end of the line (Ctrl+K).
+func (e *Editor) KillToLineEnd() {
+ rs := e.curLine()
+ if e.Cursor.Col >= len(rs) {
+ return
+ }
+ e.setLine(rs[:e.Cursor.Col])
+ e.Dirty = true
+ e.setGoal()
+ e.followCursor()
+}
+
// InsertRune inserts r at the cursor and advances it.
func (e *Editor) InsertRune(r rune) {
rs := e.curLine()
@@ -274,6 +299,10 @@ case tea.KeyEnd:
e.MoveEnd()
case tea.KeyTab:
e.InsertRune('\t')
+ case tea.KeyCtrlU:
+ e.KillToLineStart()
+ case tea.KeyCtrlK:
+ e.KillToLineEnd()
}
}
internal/editor/editor_test.go +36 −0
@@ -288,3 +288,39 @@ }
}
}
}
+
+func TestKillToLineStart(t *testing.T) {
+ e := newEditorWith("hello world")
+ e.Cursor = Position{Row: 0, Col: 6}
+ e.KillToLineStart()
+ if e.Lines[0] != "world" {
+ t.Errorf("Lines[0] = %q, want 'world'", e.Lines[0])
+ }
+ if e.Cursor.Col != 0 {
+ t.Errorf("Cursor.Col = %d, want 0", e.Cursor.Col)
+ }
+ if !e.Dirty {
+ t.Error("kill should set Dirty")
+ }
+}
+
+func TestKillToLineEnd(t *testing.T) {
+ e := newEditorWith("hello world")
+ e.Cursor = Position{Row: 0, Col: 5}
+ e.KillToLineEnd()
+ if e.Lines[0] != "hello" {
+ t.Errorf("Lines[0] = %q, want 'hello'", e.Lines[0])
+ }
+ if !e.Dirty {
+ t.Error("kill should set Dirty")
+ }
+}
+
+func TestKillToLineStartAtColZeroNoop(t *testing.T) {
+ e := newEditorWith("abc")
+ e.Cursor = Position{Row: 0, Col: 0}
+ e.KillToLineStart()
+ if e.Lines[0] != "abc" || e.Dirty {
+ t.Errorf("kill at col 0 should be a noop; line=%q dirty=%v", e.Lines[0], e.Dirty)
+ }
+}