import AppKit import ConjureKit import Highlighter @MainActor enum NotesAppearance { static let font = NSFont.monospacedSystemFont(ofSize: 15, weight: .regular) static let paragraph: NSParagraphStyle = { let style = NSMutableParagraphStyle() style.lineSpacing = 5 style.paragraphSpacing = 8 return style }() } @MainActor final class NotesController: NSViewController, NSTextViewDelegate { private let browseButton = NSButton(title: "Browse Notes", target: nil, action: nil) private let actionsButton = NSButton(title: "Actions", target: nil, action: nil) private let actions = ApplicationActionsController() private let editor = NotesTextView() private let status = NSTextField(labelWithString: "") private let noteTitle = NSTextField(labelWithString: "Untitled Note") private let formatting = NSStackView() private static let formattingKeys: [(key: String, modifiers: NSEvent.ModifierFlags)] = [ ("h", [.command, .shift]), ("b", .command), ("i", .command), ("x", [.command, .shift]), ("e", .command), ("k", [.command, .shift]), ("e", [.command, .shift]), ("b", [.command, .shift]), ("o", [.command, .shift]), ("u", [.command, .shift]), ("t", [.command, .shift]), ] private let defaults: UserDefaults private var store: NotesStore? private var selectedID: UUID? private var isDirty = false private var loadError: Error? var closed: (() -> Void)? init(defaults: UserDefaults = .standard, directory: URL? = nil) { self.defaults = defaults super.init(nibName: nil, bundle: nil) do { store = try directory.map { try NotesStore(directory: $0) } ?? NotesStore() } catch { loadError = error } } required init?(coder: NSCoder) { fatalError("Conjure creates Notes programmatically") } override func loadView() { let content = NotesView() content.commandShortcut = { [weak self] in self?.handleCommandKey($0) ?? false } view = content browseButton.target = self browseButton.action = #selector(browseNotes) configureIcon(browseButton, symbol: "note.text", label: "Browse notes (⌘P)") browseButton.keyEquivalent = "p" actionsButton.target = self actionsButton.action = #selector(showActions) configureIcon(actionsButton, symbol: "command", label: "Notes actions (⌘K)") let create = iconButton("plus", label: "New note (⌘N)", action: #selector(newNote)) let controls = NSStackView(views: [actionsButton, browseButton, create]) controls.spacing = 2 noteTitle.font = .systemFont(ofSize: 13) noteTitle.textColor = SunsetPalette.secondaryText noteTitle.alignment = .center noteTitle.lineBreakMode = .byTruncatingTail noteTitle.setAccessibilityLabel("Note title") let header = NSView() for control in [noteTitle, controls] { control.translatesAutoresizingMaskIntoConstraints = false header.addSubview(control) control.centerYAnchor.constraint(equalTo: header.centerYAnchor).isActive = true } NSLayoutConstraint.activate([ controls.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -8), noteTitle.centerXAnchor.constraint(equalTo: header.centerXAnchor), noteTitle.leadingAnchor.constraint(greaterThanOrEqualTo: header.leadingAnchor, constant: 44), noteTitle.trailingAnchor.constraint(lessThanOrEqualTo: controls.leadingAnchor, constant: -16), ]) actions.closed = { [weak self] in self?.focusEditor() } actions.selectNote = { [weak self] in self?.switchNote($0) } actions.newNote = { [weak self] in self?.newNote() } actions.browseNotes = { [weak self] in self?.browseNotes() } actions.deleteNote = { [weak self] in self?.deleteNote() } let tools = [ ("textformat.size", "Heading"), ("bold", "Bold"), ("italic", "Italic"), ("strikethrough", "Strikethrough"), ("chevron.left.forwardslash.chevron.right", "Inline code"), ("link", "Link"), ("curlybraces.square", "Code block"), ("text.quote", "Quote"), ("list.number", "Ordered list"), ("list.bullet", "Bullet list"), ("checklist", "Checklist"), ] for (index, tool) in tools.enumerated() { let button = iconButton(tool.0, label: tool.1, action: #selector(formatNote(_:))) button.tag = index let shortcut = Self.formattingKeys[index] button.toolTip = "\(tool.1) (⌘\(shortcut.modifiers.contains(.shift) ? "⇧" : "")\(shortcut.key.uppercased()))" formatting.addArrangedSubview(button) } formatting.spacing = 8 let footer = NSStackView(views: [formatting, NSView()]) footer.alignment = .centerY footer.spacing = 8 editor.delegate = self editor.isRichText = true editor.importsGraphics = true editor.allowsUndo = true editor.drawsBackground = false editor.textColor = SunsetPalette.text editor.insertionPointColor = SunsetPalette.accent editor.selectedTextAttributes = [.backgroundColor: SunsetPalette.selectionFill] editor.font = NotesAppearance.font editor.defaultParagraphStyle = NotesAppearance.paragraph editor.isAutomaticQuoteSubstitutionEnabled = false editor.isAutomaticDashSubstitutionEnabled = false editor.isAutomaticTextReplacementEnabled = false editor.isVerticallyResizable = true editor.isHorizontallyResizable = false editor.autoresizingMask = [.width] editor.textContainer?.widthTracksTextView = true editor.textContainerInset = NSSize(width: 22, height: 8) editor.setAccessibilityLabel("Note editor") let scroll = NSScrollView() scroll.documentView = editor scroll.hasVerticalScroller = true scroll.autohidesScrollers = true scroll.drawsBackground = false scroll.scrollerStyle = .overlay status.font = .systemFont(ofSize: 12) status.textColor = SunsetPalette.secondaryText status.setAccessibilityLabel("Notes save status") status.lineBreakMode = .byTruncatingTail status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) status.isHidden = true let body = NSStackView(views: [scroll, status, footer]) body.orientation = .vertical body.alignment = .leading body.spacing = 4 for control in [header, body] { control.translatesAutoresizingMaskIntoConstraints = false view.addSubview(control) } NSLayoutConstraint.activate([ header.topAnchor.constraint(equalTo: view.topAnchor), header.leadingAnchor.constraint(equalTo: view.leadingAnchor), header.trailingAnchor.constraint(equalTo: view.trailingAnchor), header.heightAnchor.constraint(equalToConstant: 38), body.topAnchor.constraint(equalTo: header.bottomAnchor, constant: 4), body.leadingAnchor.constraint(equalTo: view.leadingAnchor), body.trailingAnchor.constraint(equalTo: view.trailingAnchor), body.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -4), scroll.widthAnchor.constraint(equalTo: body.widthAnchor), footer.leadingAnchor.constraint(equalTo: body.leadingAnchor, constant: 24), footer.trailingAnchor.constraint(equalTo: body.trailingAnchor, constant: -6), status.leadingAnchor.constraint(equalTo: body.leadingAnchor, constant: 28), status.trailingAnchor.constraint(equalTo: body.trailingAnchor, constant: -12), scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 80), footer.heightAnchor.constraint(equalToConstant: 32), ]) guard let store else { editor.isEditable = false for control in [browseButton, actionsButton, create] { control.isEnabled = false } formatting.subviews.compactMap { $0 as? NSButton }.forEach { $0.isEnabled = false } showError(loadError!) return } do { let savedID = defaults.string(forKey: "selectedNote").flatMap(UUID.init(uuidString:)) let id = store.notes.first { $0.id == savedID }?.id ?? store.notes.first?.id select(try id ?? store.create()) } catch { editor.isEditable = false showError(error) } } private func configureIcon(_ button: NSButton, symbol: String, label: String) { button.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)? .withSymbolConfiguration(.init(pointSize: 16, weight: .regular)) button.imagePosition = .imageOnly button.bezelStyle = .accessoryBarAction button.showsBorderOnlyWhileMouseInside = true button.contentTintColor = SunsetPalette.secondaryText button.toolTip = label button.setAccessibilityLabel(label) NSLayoutConstraint.activate([button.widthAnchor.constraint(equalToConstant: 28), button.heightAnchor.constraint(equalToConstant: 28)]) } private func iconButton(_ symbol: String, label: String, action: Selector) -> NSButton { let button = NSButton(title: "", target: self, action: action) configureIcon(button, symbol: symbol, label: label) return button } @objc private func formatNote(_ sender: NSButton) { guard editor.isEditable else { return } let contents = editor.markdownContents let source = contents.string as NSString var selection = editor.markdownInsertionRange(for: editor.selectedRange()) if sender.tag == 6 { selection = source.lineRange(for: selection) if selection.length > 0, source.character(at: NSMaxRange(selection) - 1) == 10 { selection.length -= 1 } } let wrappers = [1: ("**", "**"), 2: ("*", "*"), 3: ("~~", "~~"), 4: ("`", "`"), 5: ("[", "](url)"), 6: ("```\n", "\n```")] if let (open, close) = wrappers[sender.tag] { let before = selection.location - open.utf16.count let after = NSMaxRange(selection) var stars = 0 if open == "*" { while selection.location > stars, after + stars < source.length, source.character(at: selection.location - stars - 1) == 42, source.character(at: after + stars) == 42 { stars += 1 } } if before >= 0, after + close.utf16.count <= source.length, source.substring(with: NSRange(location: before, length: open.utf16.count)) == open, source.substring(with: NSRange(location: after, length: close.utf16.count)) == close, open != "*" || stars % 2 == 1 { let body = contents.attributedSubstring(from: selection) selection = NSRange(location: before, length: selection.length + open.utf16.count + close.utf16.count) replaceMarkdown(body, range: selection, selection: NSRange(location: before, length: body.length)) } else { let body = NSMutableAttributedString(string: open, attributes: editor.typingAttributes) body.append(contents.attributedSubstring(from: selection)) body.append(NSAttributedString(string: close, attributes: editor.typingAttributes)) replaceMarkdown(body, range: selection, selection: NSRange(location: selection.location + open.utf16.count, length: selection.length)) } } else { let prefixes = [0: "# ", 7: "> ", 8: "1. ", 9: "- ", 10: "- [ ] "] guard let prefix = prefixes[sender.tag] else { return } let length = selection.length > 0 && source.character(at: NSMaxRange(selection) - 1) == 10 ? selection.length - 1 : selection.length let lines = source.lineRange(for: NSRange(location: selection.location, length: length)) let body = NSMutableAttributedString(attributedString: contents.attributedSubstring(from: lines)) let rows = body.string.components(separatedBy: "\n") var offset = body.length, firstDelta = 0 for (index, line) in rows.enumerated().reversed() { offset -= line.utf16.count + (index < rows.count - 1 ? 1 : 0) if index == rows.count - 1, index > 0, line.isEmpty { continue } let indent = String(line.prefix { $0 == " " || $0 == "\t" }) let content = String(line.dropFirst(indent.count)) let item = Self.listMatch(in: content as NSString) let kind = item.map { $0.range(at: 6).location != NSNotFound ? 10 : $0.range(at: 3).location != NSNotFound ? 8 : 9 } let removing = kind == sender.tag || (item == nil && content.hasPrefix(prefix)) let oldPrefix = item?.range.length ?? (removing ? prefix.utf16.count : 0) let newPrefix = removing ? "" : sender.tag == 8 ? "\(index + 1). " : prefix body.replaceCharacters(in: NSRange(location: offset + indent.utf16.count, length: oldPrefix), with: NSAttributedString(string: newPrefix, attributes: editor.typingAttributes)) if index == 0 { firstDelta = newPrefix.utf16.count - oldPrefix } } let start = max(lines.location, selection.location + firstDelta) let end = max(start, NSMaxRange(selection) + body.length - lines.length) replaceMarkdown(body, range: lines, selection: NSRange(location: start, length: end - start)) } focusEditor() } func focusEditor() { view.window?.makeFirstResponder(editor) } func textDidChange(_ notification: Notification) { isDirty = true MarkdownHighlighting.apply(to: editor) editor.refreshTasks() _ = save() } private static let listItem = try! NSRegularExpression( pattern: "^([ \\t]*)([-+*]|([0-9]+)([.)]))([ \\t]+|$)(?:\\[([ xX]?)\\]([ \\t]+|$))?" ) private static func listMatch(in line: NSString) -> NSTextCheckingResult? { guard let item = listItem.firstMatch(in: line as String, range: NSRange(location: 0, length: line.length)) else { return nil } let marker = line.substring(with: item.range(at: 2)) // Spaced thematic breaks are not list items or list ancestors. if marker == "-" || marker == "*" { let characters = (line as String).lazy.filter { !$0.isWhitespace } let bullet = marker.first! if characters.count >= 3, characters.allSatisfy({ $0 == bullet }) { return nil } } return item } private static func indentationWidth(_ indentation: String) -> Int { indentation.reduce(0) { $1 == "\t" ? $0 + 4 - $0 % 4 : $0 + 1 } } private static func outdentLength(_ indentation: String) -> Int { indentation.hasPrefix("\t") ? 1 : indentation.prefix(2).prefix { $0 == " " }.count } private static func continuedPrefix(_ item: NSTextCheckingResult, in line: NSString) -> String? { var marker = line.substring(with: item.range(at: 2)) if item.range(at: 3).location != NSNotFound { guard let number = UInt64(line.substring(with: item.range(at: 3))), number < UInt64.max else { return nil } marker = "\(number + 1)" + line.substring(with: item.range(at: 4)) } var prefix = line.substring(with: item.range(at: 1)) + marker + line.substring(with: item.range(at: 5)) if item.range(at: 6).location != NSNotFound { prefix += "[ ]" + line.substring(with: item.range(at: 7)) } return prefix } private func setSelectionWithUndo(_ selection: NSRange) { let previous = editor.selectedRange() editor.undoManager?.registerUndo(withTarget: self) { $0.setSelectionWithUndo(previous) } editor.setSelectedRange(selection) } private func replaceListMarkdown(_ replacement: String, range: NSRange, selection: NSRange) { replaceMarkdown(NSAttributedString(string: replacement, attributes: editor.typingAttributes), range: range, selection: selection) } private func replaceMarkdown(_ replacement: NSAttributedString, range: NSRange, selection: NSRange) { editor.undoManager?.beginUndoGrouping() // Native text undo restores its diff boundary, not the user's selection. setSelectionWithUndo(editor.selectedRange()) editor.replaceMarkdown(replacement, range: range) setSelectionWithUndo(editor.renderedRange(for: selection)) editor.undoManager?.endUndoGrouping() } func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { let newline = commandSelector == #selector(NSResponder.insertNewline(_:)) let indenting = commandSelector == #selector(NSResponder.insertTab(_:)) let outdenting = commandSelector == #selector(NSResponder.insertBacktab(_:)) guard textView === editor, editor.isEditable, newline || indenting || outdenting else { return false } let source = editor.markdownString as NSString let selection = editor.markdownInsertionRange(for: editor.selectedRange()) var start = 0, end = 0 source.getLineStart(&start, end: nil, contentsEnd: &end, for: NSRange(location: selection.location, length: 0)) let line = source.substring(with: NSRange(location: start, length: end - start)) as NSString guard let item = Self.listMatch(in: line), NSMaxRange(selection) <= end else { return false } let codeBlocks = MarkdownHighlighting.codeBlocks(in: source as String) guard !codeBlocks.contains(where: { NSIntersectionRange($0.range, NSRange(location: start, length: end - start)).length > 0 }) else { return false } let indent = line.substring(with: item.range(at: 1)) if indenting { let marker = editor.renderedRange(for: NSRange(location: start + item.range(at: 2).location, length: 0)).location // A literal marker still needs native Tab to complete its separator. guard editor.textStorage?.attribute(.attachment, at: marker, effectiveRange: nil) is NSTextAttachment else { return false } } if indenting || outdenting { let removed = outdenting ? Self.outdentLength(indent) : 0 guard indenting || removed > 0 else { return true } let replacement = indenting ? " " : "" let delta = replacement.utf16.count - removed var selectedStart = max(start, selection.location + delta) var selectedEnd = max(start, NSMaxRange(selection) + delta) func isListLine(_ range: NSRange) -> Bool { Self.listMatch(in: source.substring(with: range) as NSString) != nil && !codeBlocks.contains { NSIntersectionRange($0.range, range).length > 0 } } var block = source.lineRange(for: NSRange(location: start, length: 0)) while block.location > 0 { let previous = source.lineRange(for: NSRange(location: block.location - 1, length: 0)) guard isListLine(previous) else { break } block = NSUnionRange(previous, block) } while NSMaxRange(block) < source.length { let next = source.lineRange(for: NSRange(location: NSMaxRange(block), length: 0)) guard isListLine(next) else { break } block = NSUnionRange(block, next) } let body = NSMutableAttributedString(attributedString: editor.markdownContents.attributedSubstring(from: block)) body.replaceCharacters(in: NSRange(location: start - block.location, length: removed), with: replacement) let changed = body.string as NSString let oldDepth = Self.indentationWidth(indent) let newDepth = Self.indentationWidth(replacement + (indent as NSString).substring(from: removed)) var ancestors: [(depth: Int, number: UInt64?, delimiter: String)] = [] var edits: [(range: NSRange, number: String)] = [] var oldLevel = true, newLevel = true var offset = 0 while offset < changed.length { let row = changed.lineRange(for: NSRange(location: offset, length: 0)) let text = changed.substring(with: row) as NSString guard let match = Self.listMatch(in: text) else { break } let depth = Self.indentationWidth(text.substring(with: match.range(at: 1))) if offset + block.location > start { if depth < oldDepth { oldLevel = false } if depth < newDepth { newLevel = false } } while let last = ancestors.last, last.depth > depth { ancestors.removeLast() } let peer = ancestors.last?.depth == depth ? ancestors.removeLast() : nil var number: UInt64? var delimiter = "" if match.range(at: 3).location != NSNotFound { number = UInt64(text.substring(with: match.range(at: 3))) delimiter = text.substring(with: match.range(at: 4)) if offset + block.location >= start, (oldLevel && depth == oldDepth) || (newLevel && depth == newDepth) { if let peer, peer.delimiter == delimiter, let previous = peer.number, previous < UInt64.max { number = previous + 1 } else if !ancestors.isEmpty || offset + block.location > start { number = 1 } if let number, UInt64(text.substring(with: match.range(at: 3))) != number { let digits = match.range(at: 3) edits.append((NSRange(location: offset + digits.location, length: digits.length), String(number))) } } } ancestors.append((depth, number, delimiter)) offset = NSMaxRange(row) } for edit in edits.reversed() { body.replaceCharacters(in: edit.range, with: edit.number) let location = block.location + edit.range.location let difference = edit.number.utf16.count - edit.range.length func adjusted(_ position: Int) -> Int { if position <= location { return position } if position < location + edit.range.length { return location + min(position - location, edit.number.utf16.count) } return position + difference } selectedStart = adjusted(selectedStart) selectedEnd = adjusted(selectedEnd) } replaceMarkdown(body, range: block, selection: NSRange(location: selectedStart, length: selectedEnd - selectedStart)) return true } let markerEnd = item.range(at: 6).location == NSNotFound ? NSMaxRange(item.range(at: 2)) : NSMaxRange(item.range(at: 6)) + 1 guard selection.location >= start + markerEnd else { return false } let body = line.substring(from: item.range.length) if body.trimmingCharacters(in: .whitespaces).isEmpty { var replacement = indent.isEmpty ? "" : line.substring(from: Self.outdentLength(indent)) var previous = start let depth = Self.indentationWidth(indent) while depth > 0, previous > 0 { let range = source.lineRange(for: NSRange(location: previous - 1, length: 0)) previous = range.location let ancestor = source.substring(with: range) as NSString if let parent = Self.listMatch(in: ancestor), !codeBlocks.contains(where: { NSIntersectionRange($0.range, range).length > 0 }) { let parentIndent = ancestor.substring(with: parent.range(at: 1)) if Self.indentationWidth(parentIndent) < depth { replacement = Self.continuedPrefix(parent, in: ancestor) ?? "" break } } else if !(ancestor as String).trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { let leading = (ancestor as String).prefix { $0 == " " || $0 == "\t" } if Self.indentationWidth(String(leading)) < depth { break } } } replaceListMarkdown(replacement, range: NSRange(location: start, length: end - start), selection: NSRange(location: start + replacement.utf16.count, length: 0)) } else { guard let prefix = Self.continuedPrefix(item, in: line) else { return false } replaceListMarkdown("\n" + prefix, range: selection, selection: NSRange(location: selection.location + 1 + prefix.utf16.count, length: 0)) } return true } @discardableResult func save() -> Bool { guard isDirty, let store, let selectedID else { return true } do { try store.update(id: selectedID, contents: editor.markdownContents) isDirty = false noteTitle.stringValue = store.notes.first { $0.id == selectedID }?.title ?? "Untitled Note" status.isHidden = true return true } catch { showError(error) return false } } func handle(_ selector: Selector) -> Bool { actions.isShown && actions.handle(selector) } func handleControlKey(_ key: String) -> Bool { guard actions.isShown, key == "j" || key == "k" else { return false } actions.moveSelection(by: key == "j" ? 1 : -1) return true } func handleCommandKey(_ event: NSEvent) -> Bool { guard store != nil, view.window?.attachedSheet == nil else { return false } let modifiers = event.modifierFlags.intersection([.command, .control, .option, .shift]) 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, let button = formatting.arrangedSubviews[index] as? NSButton else { return false } formatNote(button) return true } guard modifiers == .command else { return false } switch key { case "p": browseNotes() case "k": showActions() case "n": newNote() default: return false } return true } @discardableResult func close() -> Bool { guard save() else { return false } actions.close(animated: false) closed?() return true } func goBack() { if actions.isShown { actions.close() } else { _ = close() } } @objc private func newNote() { actions.close(animated: false) guard save(), let store else { return } do { select(try store.create()); focusEditor() } catch { showError(error) } } private func deleteNote() { guard let window = view.window, window.attachedSheet == nil, let id = selectedID else { return } actions.close(animated: false) let alert = NSAlert() alert.alertStyle = .warning alert.messageText = "Delete “\(noteTitle.stringValue)”?" alert.informativeText = "This permanently deletes the note and its embedded images. This cannot be undone." alert.addButton(withTitle: "Cancel").keyEquivalent = "\u{1b}" let delete = alert.addButton(withTitle: "Delete Note") delete.contentTintColor = .systemRed delete.keyEquivalent = "\r" alert.window.initialFirstResponder = delete alert.window.defaultButtonCell = delete.cell as? NSButtonCell alert.beginSheetModal(for: window) { [weak self] response in guard let self else { return } defer { self.focusEditor() } guard response == .alertSecondButtonReturn, self.selectedID == id, self.save(), let store = self.store else { return } do { try store.delete(id: id) self.selectedID = nil self.defaults.removeObject(forKey: "selectedNote") self.editor.setMarkdownContents(NSAttributedString(string: "")) self.editor.undoManager?.removeAllActions() self.editor.isEditable = false self.noteTitle.stringValue = "Untitled Note" self.select(try store.notes.first?.id ?? store.create()) } catch { self.showError(error) } } } private func switchNote(_ id: UUID) { guard save() else { return } select(id) focusEditor() } private func select(_ id: UUID) { guard let note = store?.notes.first(where: { $0.id == id }) else { return } selectedID = id defaults.set(id.uuidString, forKey: "selectedNote") editor.setMarkdownContents(note.contents) editor.undoManager?.removeAllActions() editor.isEditable = true editor.setSelectedRange(NSRange(location: editor.string.utf16.count, length: 0)) MarkdownHighlighting.apply(to: editor) editor.refreshTasks() noteTitle.stringValue = note.title status.isHidden = true } private func showError(_ error: Error) { status.isHidden = false status.stringValue = "Couldn’t save, load, or delete notes: \(error.localizedDescription)" status.toolTip = status.stringValue status.textColor = .systemRed } @objc func browseNotes() { guard let store else { return } actions.showNotes(store.notes.map { (id: $0.id, title: $0.title) }, selected: selectedID, relativeTo: browseButton) } @objc func showActions() { guard store != nil else { return } actions.showNotesActions(relativeTo: actionsButton) } } @MainActor private final class NotesView: NSView { var commandShortcut: ((NSEvent) -> Bool)? override func performKeyEquivalent(with event: NSEvent) -> Bool { if commandShortcut?(event) == true { return true } return super.performKeyEquivalent(with: event) } } @MainActor enum MarkdownHighlighting { // ponytail: whole-note highlighting; incremental parsing if large notes make typing slow. static let hiddenMarkup = NSAttributedString.Key("Conjure.hiddenMarkup") private static let inlineCode = try! NSRegularExpression(pattern: "(?`+)(?
[^`\\n]+)\\k