fix: restore empty list exit and keyboard indentation
This commit is contained in:
@@ -124,10 +124,26 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
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 {
|
||||
@@ -141,38 +157,69 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
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) {
|
||||
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 {
|
||||
guard textView === editor, editor.isEditable,
|
||||
commandSelector == #selector(NSResponder.insertNewline(_:)) else { return false }
|
||||
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.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: source as String).contains(where: {
|
||||
selection.location >= $0.body.location && selection.location <= NSMaxRange($0.body)
|
||||
}) else { return false }
|
||||
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))
|
||||
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 }
|
||||
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
|
||||
let selectedStart = max(start, selection.location + delta)
|
||||
let selectedEnd = max(start, NSMaxRange(selection) + delta)
|
||||
replaceListMarkdown(replacement, range: NSRange(location: start, length: removed),
|
||||
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 = ""
|
||||
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.listItem.firstMatch(in: ancestor as String, range: NSRange(location: 0, length: ancestor.length)) {
|
||||
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) ?? ""
|
||||
@@ -183,12 +230,12 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
if Self.indentationWidth(String(leading)) < depth { break }
|
||||
}
|
||||
}
|
||||
editor.replaceMarkdown(replacement, range: NSRange(location: start, length: end - start))
|
||||
editor.setSelectedRange(editor.renderedRange(for: NSRange(location: start + replacement.utf16.count, length: 0)))
|
||||
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 }
|
||||
editor.replaceMarkdown("\n" + prefix, range: selection)
|
||||
editor.setSelectedRange(editor.renderedRange(for: NSRange(location: selection.location + 1 + prefix.utf16.count, length: 0)))
|
||||
replaceListMarkdown("\n" + prefix, range: selection,
|
||||
selection: NSRange(location: selection.location + 1 + prefix.utf16.count, length: 0))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -208,6 +208,15 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
return markdownRange(for: NSRange(location: body, length: 0))
|
||||
}
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
if event.keyCode == 48,
|
||||
event.modifierFlags.intersection([.command, .control, .option, .shift]) == .shift,
|
||||
delegate?.textView?(self, doCommandBy: #selector(NSResponder.insertBacktab(_:))) == true {
|
||||
return
|
||||
}
|
||||
super.keyDown(with: event)
|
||||
}
|
||||
|
||||
override func insertText(_ insertString: Any, replacementRange: NSRange) {
|
||||
let replacement: NSAttributedString
|
||||
if let attributed = insertString as? NSAttributedString {
|
||||
|
||||
Reference in New Issue
Block a user