feat: add modal Vim editing to notes
This commit is contained in:
@@ -22,6 +22,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
private let editor = NotesTextView()
|
||||
private let status = NSTextField(labelWithString: "")
|
||||
private let noteTitle = NSTextField(labelWithString: "Untitled Note")
|
||||
private let vimButton = NSButton(title: "Vim Off", target: nil, action: nil)
|
||||
private let formatting = NSStackView()
|
||||
private static let formattingKeys: [(key: String, modifiers: NSEvent.ModifierFlags)] = [
|
||||
("h", [.command, .shift]), ("b", .command), ("i", .command),
|
||||
@@ -64,14 +65,25 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
noteTitle.lineBreakMode = .byTruncatingTail
|
||||
noteTitle.setAccessibilityLabel("Note title")
|
||||
let header = NSView()
|
||||
for control in [noteTitle] {
|
||||
vimButton.target = self
|
||||
vimButton.action = #selector(toggleVim)
|
||||
vimButton.bezelStyle = .accessoryBarAction
|
||||
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."
|
||||
editor.vimModeChanged = { [weak self] in self?.updateVimIndicator() }
|
||||
editor.vimEnabled = defaults.bool(forKey: "notesVimEnabled")
|
||||
for control in [noteTitle, vimButton] {
|
||||
control.translatesAutoresizingMaskIntoConstraints = false
|
||||
header.addSubview(control)
|
||||
control.centerYAnchor.constraint(equalTo: header.centerYAnchor).isActive = true
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
noteTitle.centerXAnchor.constraint(equalTo: header.centerXAnchor),
|
||||
noteTitle.leadingAnchor.constraint(greaterThanOrEqualTo: header.leadingAnchor, constant: 44),
|
||||
vimButton.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 12),
|
||||
vimButton.widthAnchor.constraint(equalToConstant: 90),
|
||||
noteTitle.leadingAnchor.constraint(greaterThanOrEqualTo: vimButton.trailingAnchor, constant: 8),
|
||||
noteTitle.trailingAnchor.constraint(lessThanOrEqualTo: header.trailingAnchor, constant: -44),
|
||||
])
|
||||
actions.closed = { [weak self] in self?.focusEditor() }
|
||||
@@ -190,6 +202,18 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
showError(error)
|
||||
}
|
||||
}
|
||||
@objc private func toggleVim() {
|
||||
editor.vimEnabled.toggle()
|
||||
defaults.set(editor.vimEnabled, forKey: "notesVimEnabled")
|
||||
focusEditor()
|
||||
}
|
||||
|
||||
private func updateVimIndicator() {
|
||||
vimButton.title = editor.vimEnabled ? editor.vimMode.rawValue : "Vim Off"
|
||||
vimButton.setAccessibilityValue(editor.vimEnabled ? editor.vimMode.rawValue : "Off")
|
||||
vimButton.state = editor.vimEnabled ? .on : .off
|
||||
}
|
||||
|
||||
|
||||
private func configureIcon(_ button: NSButton, symbol: String, label: String) {
|
||||
button.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)?
|
||||
@@ -490,7 +514,11 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
}
|
||||
|
||||
func handle(_ selector: Selector) -> Bool {
|
||||
actions.isShown && actions.handle(selector)
|
||||
if actions.isShown { return actions.handle(selector) }
|
||||
if selector == #selector(NSResponder.cancelOperation(_:)), view.window?.firstResponder === editor {
|
||||
return editor.handleVimEscape()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleControlKey(_ key: String) -> Bool {
|
||||
@@ -502,6 +530,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
func handleCommandKey(_ event: NSEvent) -> Bool {
|
||||
guard store != nil, view.window?.attachedSheet == nil else { return false }
|
||||
let modifiers = event.modifierFlags.intersection([.command, .control, .option, .shift])
|
||||
if modifiers.contains(.command) { editor.clearVimCommand() }
|
||||
let key = event.charactersIgnoringModifiers?.lowercased() ?? ""
|
||||
if let index = Self.formattingKeys.firstIndex(where: { $0.key == key && $0.modifiers == modifiers }) {
|
||||
guard editor.isEditable, !actions.isShown, view.window?.firstResponder === editor,
|
||||
@@ -728,6 +757,27 @@ enum MarkdownHighlighting {
|
||||
}
|
||||
contents.removeAttribute(codeBackground, range: range)
|
||||
}
|
||||
// Whole visible tokens carry their delimiters through Vim delete/yank/change operations.
|
||||
static func completeInlineRange(_ selection: NSRange, in contents: NSAttributedString) -> NSRange {
|
||||
var range = selection
|
||||
let full = NSRange(location: 0, length: contents.length)
|
||||
let matches = ([inlineCode] + rules.map(\.0)).flatMap { $0.matches(in: contents.string, range: full) }
|
||||
var previous: NSRange
|
||||
repeat {
|
||||
previous = range
|
||||
for match in matches {
|
||||
let body = match.range(withName: "body")
|
||||
guard body.location != NSNotFound, body.length > 0,
|
||||
body.location > match.range.location, NSMaxRange(body) < NSMaxRange(match.range),
|
||||
range.location <= body.location, NSMaxRange(range) >= NSMaxRange(body),
|
||||
contents.attribute(hiddenMarkup, at: match.range.location, effectiveRange: nil) as? Bool == true
|
||||
else { continue }
|
||||
range = NSUnionRange(range, match.range)
|
||||
}
|
||||
} while range != previous
|
||||
return range
|
||||
}
|
||||
|
||||
|
||||
static func apply(to editor: NSTextView) {
|
||||
guard let storage = editor.textStorage else { return }
|
||||
|
||||
Reference in New Issue
Block a user