From 923425826d4b48f19e156e44a9a9305b61b2abe5 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Thu, 22 Oct 2026 12:08:33 +0200 Subject: [PATCH] fix: align Vim indicator and restore Normal Escape dismissal --- README.md | 6 +++--- Sources/Conjure/NotesController.swift | 8 ++++---- Sources/Conjure/NotesVim.swift | 2 +- Tests/ConjureKitTests/NotesVimTests.swift | 3 ++- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 986b5f6..685989f 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ Notes autosave locally as RTFD packages, including embedded images, under `~/Lib ### Vim editing -Click **Vim Off** at the left of the Notes header to enable modal editing. The button shows **NORMAL**, **INSERT**, **VISUAL**, or **VISUAL LINE**; click it again to disable Vim. This preference persists across reopening and app restarts. Opening or switching notes starts in Normal mode when enabled; ordinary editing is unchanged when disabled. +Click **Vim Off** at the top-right of the Notes header to enable modal editing. The button shows **NORMAL**, **INSERT**, **VISUAL**, or **VISUAL LINE**; click it again to disable Vim. This preference persists across reopening and app restarts. Opening or switching notes starts in Normal mode when enabled; ordinary editing is unchanged when disabled. | Operation | Keys | | --- | --- | @@ -212,11 +212,11 @@ Click **Vim Off** at the left of the Notes header to enable modal editing. The b | Delete characters; delete / change through line end | `x`; `D` / `C` | | Paste after / before | `p` / `P` | | Native Undo / Redo | `u` / Control-R; Command-Z / Command-Shift-Z remain available | -| Return to Normal / cancel a pending command | Escape or Control-[ | +| Return to Normal (from Insert/Visual) | Escape or Control-[ | Counts work before motions and operators, including `3j`, `2dd`, and `2d3w`. Vertical movement uses logical lines rather than wrapped display rows and retains the desired column across shorter lines. Character motions and selections respect composed Unicode characters and skip hidden Markdown syntax. Whole-token operators carry recognized inline formatting delimiters with their visible text; linewise operations include heading and list prefixes. Yank/delete/change use a local unnamed register, preserving attributed text and embedded images for `p`/`P` without replacing the system clipboard. Editing and Undo still use the native text view and RTFD autosaves. -Escape closes an open picker first; otherwise, with Vim enabled, it returns to Normal without dismissing Notes. Command-W still dismisses the window. Command shortcuts, formatting controls, and native Insert-mode input remain available. Pending commands cancel on focus changes and Command shortcuts. This is a supported Vim subset, not an embedded Vim: search, text objects, named registers, macros, and dot-repeat are not implemented. +Escape closes an open picker first. With Vim enabled, Escape returns Insert or Visual mode to Normal; in Normal mode it dismisses Notes, just as when Vim is disabled. Command-W still dismisses the window. Command shortcuts, formatting controls, and native Insert-mode input remain available. Pending commands cancel on focus changes and Command shortcuts. This is a supported Vim subset, not an embedded Vim: search, text objects, named registers, macros, and dot-repeat are not implemented. ## Translation diff --git a/Sources/Conjure/NotesController.swift b/Sources/Conjure/NotesController.swift index a7106ed..703b2f1 100644 --- a/Sources/Conjure/NotesController.swift +++ b/Sources/Conjure/NotesController.swift @@ -71,7 +71,7 @@ final class NotesController: NSViewController, NSTextViewDelegate { vimButton.font = .monospacedSystemFont(ofSize: 10, weight: .medium) vimButton.contentTintColor = SunsetPalette.secondaryText vimButton.setAccessibilityLabel("Toggle Vim editing") - vimButton.toolTip = "Toggle Vim editing. Escape: Normal mode; i: Insert; v: Visual; Command-W: close." + vimButton.toolTip = "Toggle Vim editing. Escape: Normal mode, then close; i: Insert; v: Visual." editor.vimModeChanged = { [weak self] in self?.updateVimIndicator() } editor.vimEnabled = defaults.bool(forKey: "notesVimEnabled") for control in [noteTitle, vimButton] { @@ -81,10 +81,10 @@ final class NotesController: NSViewController, NSTextViewDelegate { } NSLayoutConstraint.activate([ noteTitle.centerXAnchor.constraint(equalTo: header.centerXAnchor), - vimButton.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 12), + vimButton.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -12), vimButton.widthAnchor.constraint(equalToConstant: 90), - noteTitle.leadingAnchor.constraint(greaterThanOrEqualTo: vimButton.trailingAnchor, constant: 8), - noteTitle.trailingAnchor.constraint(lessThanOrEqualTo: header.trailingAnchor, constant: -44), + noteTitle.leadingAnchor.constraint(greaterThanOrEqualTo: header.leadingAnchor, constant: 44), + noteTitle.trailingAnchor.constraint(lessThanOrEqualTo: vimButton.leadingAnchor, constant: -8), ]) actions.closed = { [weak self] in self?.focusEditor() } actions.selectNote = { [weak self] in self?.switchNote($0) } diff --git a/Sources/Conjure/NotesVim.swift b/Sources/Conjure/NotesVim.swift index 82197ca..d251cde 100644 --- a/Sources/Conjure/NotesVim.swift +++ b/Sources/Conjure/NotesVim.swift @@ -124,7 +124,7 @@ extension NotesTextView { @discardableResult func handleVimEscape() -> Bool { - guard vimEnabled, !hasMarkedText() else { return false } + guard vimEnabled, vimState.mode != .normal, !hasMarkedText() else { return false } let buffer = VimBuffer(attributedString()) var head = vimState.head ?? buffer.index(selectedRange().location) if vimState.mode == .insert && head > buffer.lineStart(head) { head -= 1 } diff --git a/Tests/ConjureKitTests/NotesVimTests.swift b/Tests/ConjureKitTests/NotesVimTests.swift index 8774914..80e5220 100644 --- a/Tests/ConjureKitTests/NotesVimTests.swift +++ b/Tests/ConjureKitTests/NotesVimTests.swift @@ -208,7 +208,8 @@ func vimPreferenceEscapePickersAndNativeShortcutsCoexist() throws { try vimKeys("w", editor: editor) #expect(editor.markdownString == beforeMotion) try vimKeys("d", editor: editor) - launcher.dismiss(restoringFocus: false) + window.sendEvent(escape) + #expect(!window.isVisible) launcher.present(command: .notes) let reopenedContent = try #require(window.contentView) let reopened = try #require(descendants(reopenedContent).compactMap { $0 as? NotesTextView }.first)