feat: continue nested markdown lists on return
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user