feat: improve note formatting shortcuts and code styling

This commit is contained in:
2026-10-22 09:35:51 +02:00
parent 5065ba59bd
commit 665691ddee
5 changed files with 87 additions and 22 deletions
+21 -13
View File
@@ -25,9 +25,10 @@ final class NotesController: NSViewController, NSTextViewDelegate {
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]),
("s", [.command, .shift]), ("e", .command), ("k", [.command, .shift]),
("e", [.command, .shift]), ("b", [.command, .shift]),
("o", [.command, .shift]), ("u", [.command, .shift]), ("t", [.command, .shift]),
("o", [.command, .shift]), ("l", [.command, .shift]), ("t", [.command, .shift]),
("u", [.command, .shift]),
]
private let defaults: UserDefaults
private var store: NotesStore?
@@ -85,6 +86,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
("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"),
("underline", "Underline"),
]
for (index, tool) in tools.enumerated() {
let button = iconButton(tool.0, label: tool.1, action: #selector(formatNote(_:)))
@@ -219,7 +221,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
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```")]
5: ("[", "](url)"), 6: ("```\n", "\n```"), 11: ("<u>", "</u>")]
if let (open, close) = wrappers[sender.tag] {
let before = selection.location - open.utf16.count
let after = NSMaxRange(selection)
@@ -677,12 +679,14 @@ private final class RoundedImageAttachmentCell: NSTextAttachmentCell {
enum MarkdownHighlighting {
// ponytail: whole-note highlighting; incremental parsing if large notes make typing slow.
static let hiddenMarkup = NSAttributedString.Key("Conjure.hiddenMarkup")
static let codeBackground = NSAttributedString.Key("Conjure.codeBackground")
private static let inlineCode = try! NSRegularExpression(pattern: "(?<![\\\\`])(?<marker>`+)(?<body>[^`\\n]+)\\k<marker>(?!`)")
private static let rules: [(NSRegularExpression, [NSAttributedString.Key: Any])] = [
(try! NSRegularExpression(pattern: "(?<![\\\\*_])(?<marker>\\*{2,3}|_{2,3})(?<body>\\S(?:[^\\n]*?\\S)?)\\k<marker>(?![*_])"), [.font: NSFont.monospacedSystemFont(ofSize: NotesAppearance.font.pointSize, weight: .bold)]),
(try! NSRegularExpression(pattern: "(?<![\\\\*_])(?<marker>[*_])(?<body>[^\\s*_](?:[^*_\\n]*?[^\\s*_])?)\\k<marker>(?![*_])"), [.font: NSFontManager.shared.convert(NotesAppearance.font, toHaveTrait: .italicFontMask)]),
(try! NSRegularExpression(pattern: "(?<!\\\\)!?\\[(?<body>[^\\]\\n]+)\\]\\([^\\n)]+\\)|(?m)^\\s*(?:[-+*]|[0-9]+\\.)\\s"), [.foregroundColor: SunsetPalette.accent]),
(try! NSRegularExpression(pattern: "(?<!\\\\)~~(?<body>\\S(?:[^\\n]*?\\S)?)~~"), [.strikethroughStyle: NSUnderlineStyle.single.rawValue]),
(try! NSRegularExpression(pattern: "(?<!\\\\)<u>(?<body>[^\\n]+?)</u>"), [.underlineStyle: NSUnderlineStyle.single.rawValue]),
(try! NSRegularExpression(pattern: "(?m)^(?:>[ \\t]*)+(?<body>[^\\n]+)"), [:]),
]
private static let heading = try! NSRegularExpression(pattern: "(?m)^(#{1,6}) (?<body>.+)$")
@@ -717,6 +721,14 @@ enum MarkdownHighlighting {
return blocks
}
static func removeCodeBackground(from contents: NSMutableAttributedString) {
let range = NSRange(location: 0, length: contents.length)
contents.enumerateAttribute(codeBackground, in: range) { value, run, _ in
if value as? Bool == true { contents.removeAttribute(.backgroundColor, range: run) }
}
contents.removeAttribute(codeBackground, range: range)
}
static func apply(to editor: NSTextView) {
guard let storage = editor.textStorage else { return }
let text = editor.string
@@ -735,6 +747,9 @@ enum MarkdownHighlighting {
}
}
storage.removeAttribute(.strikethroughStyle, range: range)
storage.removeAttribute(.underlineStyle, range: range)
removeCodeBackground(from: storage)
storage.addAttribute(codeBackground, value: false, range: range)
storage.addAttribute(hiddenMarkup, value: false, range: range)
storage.addAttributes([.font: NotesAppearance.font, .foregroundColor: SunsetPalette.text,
.paragraphStyle: NotesAppearance.paragraph], range: range)
@@ -755,7 +770,9 @@ enum MarkdownHighlighting {
!blocks.contains { NSIntersectionRange($0.range, match.range).length > 0 }
}
for match in codeSpans {
storage.addAttributes([.font: NotesAppearance.font, .foregroundColor: NSColor.systemGreen], range: match.range)
let body = match.range(withName: "body")
storage.addAttributes([.font: NotesAppearance.font, .foregroundColor: SunsetPalette.accent,
.backgroundColor: SunsetPalette.selectionFill, codeBackground: true], range: body)
hideSyntax(match)
}
for (expression, attributes) in rules {
@@ -781,15 +798,6 @@ enum MarkdownHighlighting {
}
}
for block in blocks {
let source = text as NSString
let opening = source.lineRange(for: NSRange(location: block.range.location, length: 0))
let openingText = source.substring(with: opening).trimmingCharacters(in: .newlines)
storage.addAttribute(hiddenMarkup, value: true,
range: NSRange(location: opening.location, length: openingText.utf16.count))
if NSMaxRange(block.body) < NSMaxRange(block.range) {
storage.addAttribute(hiddenMarkup, value: true,
range: NSRange(location: NSMaxRange(block.body), length: NSMaxRange(block.range) - NSMaxRange(block.body)))
}
storage.addAttributes([.font: NotesAppearance.font,
.foregroundColor: SunsetPalette.secondaryText], range: block.range)
storage.addAttribute(.foregroundColor, value: SunsetPalette.text, range: block.body)
+4
View File
@@ -74,6 +74,7 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
private static func serialize(_ contents: NSAttributedString, keepingMetadata: Bool = false) -> NSAttributedString {
let result = NSMutableAttributedString(attributedString: contents)
restoreListLayout(in: result)
MarkdownHighlighting.removeCodeBackground(from: result)
result.removeAttribute(MarkdownHighlighting.hiddenMarkup, range: NSRange(location: 0, length: result.length))
var replacements: [(NSRange, ListAttachment)] = []
contents.enumerateAttribute(.attachment, in: NSRange(location: 0, length: contents.length)) { value, range, _ in
@@ -210,6 +211,9 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
}
attributes.removeValue(forKey: listWhitespace)
attributes.removeValue(forKey: MarkdownHighlighting.hiddenMarkup)
if attributes.removeValue(forKey: MarkdownHighlighting.codeBackground) as? Bool == true {
attributes.removeValue(forKey: .backgroundColor)
}
return attributes
}