fix: skip invisible list separator caret stops
This commit is contained in:
@@ -47,11 +47,13 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
)
|
||||
private var tasks: [(range: NSRange, attachment: ListAttachment)] = []
|
||||
private var checkboxes: [NSButton] = []
|
||||
private var horizontalSelection: (anchor: Int, head: Int)?
|
||||
|
||||
var markdownContents: NSAttributedString { Self.serialize(attributedString()) }
|
||||
var markdownString: String { markdownContents.string }
|
||||
|
||||
func setMarkdownContents(_ contents: NSAttributedString) {
|
||||
horizontalSelection = nil
|
||||
textStorage?.setAttributedString(Self.render(contents))
|
||||
refreshTasks()
|
||||
}
|
||||
@@ -255,7 +257,8 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
|
||||
override func deleteBackward(_ sender: Any?) {
|
||||
let selection = selectedRange()
|
||||
if selection.length == 0, let task = tasks.first(where: { NSMaxRange($0.range) == selection.location }) {
|
||||
let boundary = listWhitespaceBoundary(from: selection.location, forward: false)
|
||||
if selection.length == 0, let task = tasks.first(where: { NSMaxRange($0.range) == boundary }) {
|
||||
expand(task, backwards: true)
|
||||
} else if selection.length > 0 {
|
||||
replaceMarkdown("", range: markdownRange(for: selection))
|
||||
@@ -287,35 +290,123 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
if !backwards { setSelectedRange(NSRange(location: task.range.location, length: 0)) }
|
||||
}
|
||||
|
||||
// Null layout glyphs must still be individual editable source positions.
|
||||
private func moveAcrossListPrefix(forward: Bool, selecting: Bool) -> Bool {
|
||||
// Only separator glyphs are null; indentation and body whitespace remain native stops.
|
||||
private func listWhitespaceBoundary(from position: Int, forward: Bool) -> Int {
|
||||
guard let storage = textStorage else { return position }
|
||||
let index = forward ? position : position - 1
|
||||
guard index >= 0, index < storage.length else { return position }
|
||||
var range = NSRange()
|
||||
guard storage.attribute(listWhitespace, at: index, effectiveRange: &range) as? Bool == true else {
|
||||
return position
|
||||
}
|
||||
return forward ? NSMaxRange(range) : range.location
|
||||
}
|
||||
|
||||
private func visualListBoundary(from position: Int, forward: Bool) -> Int? {
|
||||
let boundary = listWhitespaceBoundary(from: position, forward: forward)
|
||||
guard let task = tasks.first(where: {
|
||||
forward ? $0.range.location == boundary : NSMaxRange($0.range) == boundary
|
||||
}) else { return nil }
|
||||
return forward ? listWhitespaceBoundary(from: NSMaxRange(task.range), forward: true) : task.range.location
|
||||
}
|
||||
|
||||
override func setSelectedRange(_ charRange: NSRange) {
|
||||
horizontalSelection = nil
|
||||
super.setSelectedRange(charRange)
|
||||
}
|
||||
|
||||
override func setSelectedRange(_ charRange: NSRange, affinity: NSSelectionAffinity, stillSelecting: Bool) {
|
||||
horizontalSelection = nil
|
||||
super.setSelectedRange(charRange, affinity: affinity, stillSelecting: stillSelecting)
|
||||
}
|
||||
|
||||
override func setSelectedRanges(_ ranges: [NSValue], affinity: NSSelectionAffinity, stillSelecting: Bool) {
|
||||
horizontalSelection = nil
|
||||
super.setSelectedRanges(ranges, affinity: affinity, stillSelecting: stillSelecting)
|
||||
}
|
||||
|
||||
private func moveNatively(forward: Bool, selecting: Bool, sender: Any?) {
|
||||
if selecting {
|
||||
if forward { super.moveRightAndModifySelection(sender) }
|
||||
else { super.moveLeftAndModifySelection(sender) }
|
||||
} else {
|
||||
if forward { super.moveRight(sender) }
|
||||
else { super.moveLeft(sender) }
|
||||
}
|
||||
}
|
||||
|
||||
private func moveHorizontally(forward: Bool, selecting: Bool, sender: Any?) {
|
||||
let selection = selectedRange()
|
||||
guard selection.length == 0 else { return false }
|
||||
let source = string as NSString
|
||||
let index = forward ? selection.location : selection.location - 1
|
||||
guard index >= 0, index < source.length else { return false }
|
||||
let attributes = textStorage?.attributes(at: index, effectiveRange: nil) ?? [:]
|
||||
guard attributes[.attachment] is ListAttachment || attributes[listWhitespace] as? Bool == true else { return false }
|
||||
let character = source.rangeOfComposedCharacterSequence(at: index)
|
||||
setSelectedRange(selecting ? character
|
||||
: NSRange(location: forward ? NSMaxRange(character) : character.location, length: 0))
|
||||
return true
|
||||
let tracked = selecting ? horizontalSelection : nil
|
||||
if !selecting { horizontalSelection = nil }
|
||||
let anchor: Int
|
||||
let head: Int
|
||||
var nativeHead: Int?
|
||||
if let tracked {
|
||||
anchor = tracked.anchor
|
||||
head = tracked.head
|
||||
} else if selecting, selection.length > 0 {
|
||||
// Let AppKit identify the active end of a mouse or otherwise native selection.
|
||||
moveNatively(forward: forward, selecting: true, sender: sender)
|
||||
let moved = selectedRange()
|
||||
guard moved != selection else { return }
|
||||
anchor = moved.location == selection.location ? selection.location : NSMaxRange(selection)
|
||||
head = anchor == selection.location ? NSMaxRange(selection) : selection.location
|
||||
nativeHead = moved.location == anchor ? NSMaxRange(moved) : moved.location
|
||||
} else {
|
||||
anchor = selection.location
|
||||
head = selection.location
|
||||
}
|
||||
|
||||
if (selecting || selection.length == 0),
|
||||
let target = visualListBoundary(from: head, forward: forward) {
|
||||
if selecting { setHorizontalSelection(anchor: anchor, head: target) }
|
||||
else { setSelectedRange(NSRange(location: target, length: 0)) }
|
||||
return
|
||||
}
|
||||
|
||||
// Native movement supplies composed-character, bidi, and line-boundary behavior.
|
||||
// Starting inside a null run already means "after the marker", so advance into the body.
|
||||
let origin = listWhitespaceBoundary(from: head, forward: true)
|
||||
if let nativeHead, origin == head {
|
||||
let target = listWhitespaceBoundary(from: nativeHead, forward: true)
|
||||
if target != nativeHead { setHorizontalSelection(anchor: anchor, head: target) }
|
||||
return
|
||||
}
|
||||
if tracked != nil || nativeHead != nil || (selection.length == 0 && origin != head) {
|
||||
setSelectedRange(NSRange(location: origin, length: 0))
|
||||
}
|
||||
moveNatively(forward: forward, selecting: selecting, sender: sender)
|
||||
let moved = selectedRange()
|
||||
let newHead = selecting && moved.location == origin ? NSMaxRange(moved) : moved.location
|
||||
let target = listWhitespaceBoundary(from: newHead, forward: true)
|
||||
if selecting {
|
||||
if tracked != nil || nativeHead != nil || target != newHead || origin != head {
|
||||
setHorizontalSelection(anchor: anchor, head: target)
|
||||
}
|
||||
} else if target != newHead { setSelectedRange(NSRange(location: target, length: 0)) }
|
||||
}
|
||||
|
||||
private func setHorizontalSelection(anchor: Int, head: Int) {
|
||||
setSelectedRange(NSRange(location: min(anchor, head), length: abs(head - anchor)),
|
||||
affinity: selectionAffinity, stillSelecting: false)
|
||||
horizontalSelection = (anchor, head)
|
||||
}
|
||||
|
||||
override func moveRight(_ sender: Any?) {
|
||||
if !moveAcrossListPrefix(forward: true, selecting: false) { super.moveRight(sender) }
|
||||
moveHorizontally(forward: true, selecting: false, sender: sender)
|
||||
}
|
||||
|
||||
override func moveLeft(_ sender: Any?) {
|
||||
if !moveAcrossListPrefix(forward: false, selecting: false) { super.moveLeft(sender) }
|
||||
moveHorizontally(forward: false, selecting: false, sender: sender)
|
||||
}
|
||||
|
||||
override func moveRightAndModifySelection(_ sender: Any?) {
|
||||
if !moveAcrossListPrefix(forward: true, selecting: true) { super.moveRightAndModifySelection(sender) }
|
||||
moveHorizontally(forward: true, selecting: true, sender: sender)
|
||||
}
|
||||
|
||||
override func moveLeftAndModifySelection(_ sender: Any?) {
|
||||
if !moveAcrossListPrefix(forward: false, selecting: true) { super.moveLeftAndModifySelection(sender) }
|
||||
moveHorizontally(forward: false, selecting: true, sender: sender)
|
||||
}
|
||||
|
||||
private func markdownSelectionWriter() -> NSTextView {
|
||||
@@ -354,6 +445,7 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
}
|
||||
|
||||
override func didChangeText() {
|
||||
horizontalSelection = nil
|
||||
refreshTasks()
|
||||
super.didChangeText()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user