feat: continue nested markdown lists on return

This commit is contained in:
Valdemar Lorenzen
2026-10-19 11:11:32 +02:00
parent 9ce2997c19
commit 8e1546d502
3 changed files with 165 additions and 2 deletions
+72 -1
View File
@@ -119,6 +119,77 @@ final class NotesController: NSViewController, NSTextViewDelegate {
_ = save()
}
private static let listItem = try! NSRegularExpression(
pattern: "^([ \\t]*)([-+*]|([0-9]+)([.)]))([ \\t]+|$)(?:\\[([ xX])\\]([ \\t]+|$))?"
)
private static func indentationWidth(_ indentation: String) -> Int {
indentation.reduce(0) { $1 == "\t" ? $0 + 4 - $0 % 4 : $0 + 1 }
}
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
}
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
guard textView === editor, editor.isEditable,
commandSelector == #selector(NSResponder.insertNewline(_:)) else { return false }
let source = editor.string as NSString
let selection = 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.listItem.firstMatch(in: line as String, range: NSRange(location: 0, length: line.length)),
selection.location >= start + item.range.length, NSMaxRange(selection) <= end,
!MarkdownHighlighting.codeBlocks(in: editor.string).contains(where: {
selection.location >= $0.body.location && selection.location <= NSMaxRange($0.body)
}) else { return false }
let indent = line.substring(with: item.range(at: 1))
let marker = line.substring(with: item.range(at: 2))
// Spaced thematic breaks are not list items.
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 false }
}
let body = line.substring(from: item.range.length)
if body.trimmingCharacters(in: .whitespaces).isEmpty {
var replacement = ""
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.listItem.firstMatch(in: ancestor as String, range: NSRange(location: 0, length: ancestor.length)) {
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 }
}
}
editor.insertText(replacement, replacementRange: NSRange(location: start, length: end - start))
} else {
guard let prefix = Self.continuedPrefix(item, in: line) else { return false }
editor.insertText("\n" + prefix, replacementRange: selection)
}
return true
}
@discardableResult
func save() -> Bool {
guard isDirty, let store, let selectedID else { return true }
@@ -244,7 +315,7 @@ enum MarkdownHighlighting {
}()
private static let openingFence = try! NSRegularExpression(pattern: "(?m)^ {0,3}(`{3,}|~{3,})([^\\r\\n]*)\\r?\\n")
private static func codeBlocks(in text: String) -> [(range: NSRange, body: NSRange, language: String?)] {
fileprivate static func codeBlocks(in text: String) -> [(range: NSRange, body: NSRange, language: String?)] {
let source = text as NSString
var blocks: [(NSRange, NSRange, String?)] = []
var cursor = 0