Skip to content

macOS ⌘⌫: match Zed editor::DeleteToBeginningOfLine #10

Description

@chasehuh

Summary

On macOS, ⌘⌫ (Cmd+Backspace) in the CodeMirror 6 note editor must match Zed’s default Editor binding exactly: editor::DeleteToBeginningOfLine (hard / logical line start — not soft-wrap boundary, not full DeleteLine).

Why This Matters

Line-kill chords are high-frequency when editing notes. After the CM6 cutover, ⌘⌫ feels wrong vs Zed. This is an explicit product keymap decision with Zed as the single source of truth — not “macOS TextEdit vaguely” and not CM’s stock soft-wrap-aware default.

Conversation Context

  • Repo: chasehuh/memo; editor: CM6 from CodeMirror 6 inline Markdown image widgets #8.
  • User (macOS): wants ⌘⌫ to behave like Zed (where it “works correctly”); earlier wording (“한 줄이 통째로 사라지길”) described the feel of clearing a line from EOL, not Zed’s separate full-line-delete chord.
  • Companion layout bug (gutter/wrap): CM6 gutter/content vertical desync with soft wrap (scroll-beyond padding) #9 — still retest with wrap on, but desired semantics are hard-line (so soft-wrap boundary bugs must not redefine the product rule).
  • App-level window shortcuts in memo-app.tsx only handle ⌘N / ⌘B / ⌘\ — they do not intercept Backspace.

Source of truth: Zed (locked)

Verified against zed-industries/zed@7cf50a771f54427f76b4584030c7b3b66f4e39f5 (main at research time).

Default macOS keymap (Editor context)

Chord Action File
⌘⌫ cmd-backspace editor::DeleteToBeginningOfLine assets/keymaps/default-macos.json L74
⌘⌦ cmd-delete editor::DeleteToEndOfLine L75
⌥⌫ alt-backspace editor::DeleteToPreviousWordStart (ignore_newlines: false) L76
⇧⌘K cmd-shift-k editor::DeleteLine (full hard-line remove — different chord) L580

Do not conflate: JetBrains keymap in Zed maps cmd-backspaceeditor::DeleteLine (assets/keymaps/macos/jetbrains.json). That is not the default. Memo follows default-macos, not JetBrains.

Action implementation

DeleteToBeginningOfLine — doc: “Deletes from the cursor to the beginning of the current line.” Field stop_at_indent defaults to false; default keymap binds the action with no args.

Implementation delete_to_beginning_of_line:

  1. Mark each selection reversed = true
  2. select_to_beginning_of_line with stop_at_soft_wraps: false (hardcoded) and stop_at_indent: action.stop_at_indent (default false)
  3. backspace (deletes the resulting selection; if still empty, deletes one unit left — typically the preceding newline)

Line target uses indented_line_beginning / line_beginning: with stop_at_soft_boundaries: false, the target is always the logical / hard line start, never a soft-wrap visual boundary.

Covering tests: test_beginning_end_of_line, test_delete_to_line_boundary (selection cases), soft-wrap ignore behavior for the same movement flags in test_beginning_end_of_line_ignore_soft_wrap.


Desired Behavior (Zed-locked — supersedes earlier waffle)

Previous “Needs verification / recommended default / alternative bind ⌘⌫ to deleteLine” language is superseded. Product choice is fixed:

⌘⌫ → Zed editor::DeleteToBeginningOfLine

Exact semantic rules:

  1. Hard line only. Ignore soft-wrap visual boundaries. From mid-wrap, delete through to the logical line start (column 0 of that hard line), not the start of the current visual wrap row.
  2. No indent stop. Default binding does not stop at first non-whitespace (stop_at_indent: false). Indentation before the cursor is deleted too.
  3. Cursor mid-line / at EOL. Delete all text from hard BOL → cursor; leave an empty hard line; cursor at BOL. From EOL this clears the whole line’s text (empty line remains) — the “line went away” feel without removing the linebreak.
  4. Cursor already at hard BOL (empty selection). Do not no-op: delete one unit backward (normally the preceding \n), joining with the previous line. At document start, nothing left to delete.
  5. Non-empty selection (Zed-specific). Do not only delete the selected span. Zed reverses the selection, extends the head to hard BOL, then deletes → effectively hard BOL → selection’s right edge. Example from Zed’s test: "one «two three» four" + action → " four".
  6. Multiple cursors. Apply per selection/cursor (Zed tests exercise multi-cursor delete-to-BOL).
  7. Newlines. Only crossed when already at hard BOL (join) or when a multi-line selection’s right edge is past newlines after the BOL expansion. Mid-line ⌘⌫ does not delete into the previous line.
  8. Undo. One transactional undo step for the kill (Zed wraps in transact).

Related chords (do not overload onto ⌘⌫)

Chord Memo target Notes
⇧⌘K CM deleteLine ≈ Zed editor::DeleteLine Removes entire hard line including linebreak; line literally disappears. Cursor lands on following (or previous) line.
⌥⌫ CM deleteGroupBackward ≈ Zed DeleteToPreviousWordStart Word/group delete; ignore_newlines: false in Zed default.
⌘⌦ Optional parity: delete to hard EOL Zed DeleteToEndOfLine also uses stop_at_soft_wraps: false. Nice-to-have, not required to close this issue.

Observable contract (QA)

  • Mid-line + ⌘⌫ → text BOL..cursor gone; empty remainder of line kept.
  • EOL + ⌘⌫ → line text cleared; empty hard line remains.
  • Empty line / caret at BOL + ⌘⌫ → join with previous line (line “removed”).
  • Soft-wrap on + caret mid visual wrap → still deletes to hard BOL (may clear text across multiple visual rows of one hard line).
  • Selection "one two three four" with "two three" selected + ⌘⌫ → " four" (BOL→selection end), not "one four".
  • ⇧⌘K still full line delete; ⌥⌫ still word/group; ⌘N/⌘B/⌘\ unaffected.
  • Hangul IME: while view.composing, do not corrupt composition.

Current Behavior (memo / CM today)

What CodeMirror ships

From @codemirror/commands standardKeymap / defaultKeymap:

Chord (macOS) Command Effect vs Zed
⌘⌫ deleteLineBoundaryBackward Soft-wrap–aware boundary via view.moveToLineBoundary. Mismatch with Zed hard-line rule.
deleteToLineStart (exported, not bound to ⌘⌫) Uses lineBlockAt(head).from (hard line for normal text). Closer for empty cursors; see gaps below.
⌥⌫ deleteGroupBackward OK keep.
⇧⌘K deleteLine Matches Zed’s separate full-line delete chord.

CM deleteBy (used by both delete helpers): non-empty selection → delete selection only. That diverges from Zed rule (5) above.

What memo does today

components/codemirror-editor.tsx installs filtered defaultKeymap (Tab overridden). macOS { mac: "Mod-Backspace", run: deleteLineBoundaryBackward } is kept — so memo currently follows CM soft-wrap boundary, not Zed.


CodeMirror mapping (implementation guidance)

Approach Empty cursor / hard line Soft wrap Non-empty selection Verdict
Stock deleteLineBoundaryBackward Soft-wrap boundary Soft Delete selection only Reject — not Zed
Stock deleteToLineStart Hard line start + BOL→join Hard (via line block) Delete selection only Close for caret-only; fails Zed selection expansion
Custom command (required for exact parity) doc.lineAt(head).from → head; if at from, delete from-1 Hard Expand each range to line.from .. range.to then delete (mirror Zed reverse+select-to-BOL) Required for SoT

Recommendation: implement a small custom command (e.g. zedDeleteToBeginningOfLine) + Prec.high macOS binding for Mod-Backspace with preventDefault: true. Keep CM deleteLine on ⇧⌘K. Optionally leave or remove the stock mac-only deleteLineBoundaryBackward binding so it cannot win on precedence.

Suggested module (optional):

// lib/editor/line-kill.ts
export function memoLineKillKeymap(): Extension
// Prec.high:
// - Mod-Backspace → zedDeleteToBeginningOfLine (custom)
// - Shift-Mod-k → deleteLine (ensure present)

Implementation Notes

Likely files

  • components/codemirror-editor.tsxPrec.high(keymap.of([…])) for macOS line-kill.
  • lib/editor/line-kill.ts (new) — custom Zed-parity command.
  • README — document ⌘⌫ = delete to hard line start (Zed); ⇧⌘K = delete line.

Flow

  1. Reproduce current CM soft-wrap / selection behavior.
  2. Add custom command + high-precedence binding.
  3. QA matrix below; retest after CM6 gutter/content vertical desync with soft wrap (scroll-beyond padding) #9 if wrap geometry was misleading during debugging.

Tests

  • Manual checklist in PR (required), including selection case from Zed’s test.
  • Optional unit test of pure “compute delete range(s)” helper.

Edge Cases And Risks

  • Soft wrap on: must still target hard BOL (product rule).
  • Multi-selections: apply per range like Zed.
  • Image widget / markdown lines: deleting line text / joining must keep decorations consistent.
  • IME composition: skip or no-op safely while composing.
  • Do not bind ⌘⌫ to deleteLine (that is ⇧⌘K / JetBrains-keymap territory).

Non-Goals

Acceptance Criteria

  • ⌘⌫ matches Zed DeleteToBeginningOfLine rules (1)–(8) above, including hard-line + selection expands to BOL.
  • PR cites Zed default-macos + input.rs implementation (or this issue’s SoT section).
  • ⇧⌘K remains full hard-line deleteLine.
  • ⌥⌫ remains word/group delete.
  • App shortcuts ⌘N / ⌘B / ⌘\ still work.
  • Undo restores the kill in one step when possible.
  • Soft-wrap on: no soft-wrap-boundary-only deletes for ⌘⌫.
  • Empty line / caret at BOL + ⌘⌫ joins as documented.

QA Plan

  1. Worktree from latest main; pnpm dev.
  2. Matrix: wrap on/off × caret at BOL / mid / EOL × empty vs non-empty × ASCII vs Hangul.
  3. Selection case: one two three four with two three selected → expect four.
  4. Soft-wrap: long hard line wrapped; caret on 2nd visual row → ⌘⌫ clears back to hard BOL (may clear text from prior visual rows of same hard line).
  5. ⇧⌘K removes the hard line entirely; compare to ⌘⌫ clearing text only.
  6. Regression: paste image, arrow ligatures, Tab spaces, save/sync.

Suggested PR Scope

S — “fix macOS ⌘⌫ to match Zed DeleteToBeginningOfLine”. Link #9 for wrap QA.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions