fix: skip invisible list separator caret stops

This commit is contained in:
Valdemar Lorenzen
2026-10-19 13:24:01 +02:00
parent e14b195856
commit ac28b6dd61
3 changed files with 235 additions and 20 deletions
+125 -2
View File
@@ -514,6 +514,129 @@ func atomicListMarkersExpandAtDeletionBoundariesAndNormalizeLineJoinsInOneUndo()
}
}
@Test @MainActor
func listNavigationSkipsHiddenSeparatorsAndKeepsSelectionAnchorsAndDeletionUndo() throws {
_ = NSApplication.shared
let suite = "NotesVisualCaret.\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(suite)
defer {
defaults.removePersistentDomain(forName: suite)
try? FileManager.default.removeItem(at: directory)
}
let controller = NotesController(defaults: defaults, directory: directory)
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 390),
styleMask: .borderless, backing: .buffered, defer: false)
window.contentViewController = controller
defer { _ = controller.close(); window.orderOut(nil) }
func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) }
let editor = try #require(descendants(controller.view).compactMap { $0 as? NotesTextView }.first)
window.makeFirstResponder(editor)
let board = NSPasteboard.withUniqueName()
defer { board.releaseGlobally() }
let body = "🦊e\u{301} z"
// Prefix source, rendered prefix, marker, separator, backward/forward expansions.
let fixtures = [
("", "", "-", " \t", "", ""),
("\t", "\t", "+", "\t ", "", ""),
(" ", " ", "009)", "\t \t", "009", "09)"),
("- parent\n ", "\u{fffc} parent\n ", "- [X]", " \t ", "- [X", " [X]"),
("\t", "\t", "12)\t[]", "\t \t", "12)\t[", "2)\t[]"),
]
for (prefix, renderedPrefix, marker, separator, backwardExpansion, forwardExpansion) in fixtures {
let source = prefix + marker + separator + body
editor.insertText(source, replacementRange: NSRange(location: 0, length: editor.string.utf16.count))
let rendered = renderedPrefix + "\u{fffc}" + separator + body
#expect(editor.string == rendered)
let before = renderedPrefix.utf16.count
let start = before + 1 + separator.utf16.count
func caret(_ index: Int) { editor.setSelectedRange(NSRange(location: index, length: 0)) }
func expectSelection(_ location: Int, _ length: Int = 0) {
#expect(editor.selectedRange() == NSRange(location: location, length: length))
}
caret(before)
editor.moveRight(nil)
expectSelection(start)
editor.moveRight(nil)
expectSelection(start + 2)
editor.moveRight(nil)
expectSelection(start + 4)
editor.moveRight(nil)
expectSelection(start + 5) // Visible body spaces still have individual stops.
editor.moveLeft(nil)
expectSelection(start + 4)
editor.moveLeft(nil)
expectSelection(start + 2)
editor.moveLeft(nil)
expectSelection(start)
editor.moveLeft(nil)
expectSelection(before)
if before > 0 {
editor.moveLeft(nil)
expectSelection(before - 1)
editor.moveRight(nil)
expectSelection(before)
}
editor.moveRightAndModifySelection(nil)
expectSelection(before, start - before)
#expect(editor.writeSelection(to: board, types: editor.writablePasteboardTypes))
#expect(board.string(forType: .string) == marker + separator)
editor.moveRightAndModifySelection(nil)
expectSelection(before, start + 2 - before)
editor.moveRightAndModifySelection(nil)
expectSelection(before, start + 4 - before)
editor.moveLeftAndModifySelection(nil)
expectSelection(before, start + 2 - before)
editor.moveLeftAndModifySelection(nil)
expectSelection(before, start - before)
editor.moveLeftAndModifySelection(nil)
expectSelection(before)
if before > 0 {
editor.moveLeftAndModifySelection(nil)
expectSelection(before - 1, 1)
editor.moveRightAndModifySelection(nil)
expectSelection(before)
}
caret(start)
editor.moveLeftAndModifySelection(nil)
expectSelection(before, start - before)
editor.moveRightAndModifySelection(nil)
expectSelection(start)
editor.moveRightAndModifySelection(nil)
expectSelection(start, 2)
editor.moveLeftAndModifySelection(nil)
expectSelection(start)
editor.moveLeftAndModifySelection(nil)
expectSelection(before, start - before)
editor.moveRightAndModifySelection(nil)
expectSelection(start)
editor.setSelectedRange(NSRange(location: 0, length: rendered.utf16.count))
board.clearContents()
#expect(editor.writeSelection(to: board, types: editor.writablePasteboardTypes))
#expect(board.string(forType: .string) == source)
#expect(controller.save())
let persisted = try NotesStore(directory: directory)
#expect(persisted.notes.contains { $0.contents.string == source })
for backward in [true, false] {
editor.setMarkdownContents(NSAttributedString(string: source))
editor.breakUndoCoalescing()
editor.undoManager?.removeAllActions()
caret(before)
if backward { editor.moveRight(nil); editor.deleteBackward(nil) }
else { editor.deleteForward(nil) }
editor.breakUndoCoalescing()
let expansion = backward ? backwardExpansion : forwardExpansion
#expect(editor.markdownString == prefix + expansion + separator + body)
#expect(editor.selectedRange() == NSRange(
location: before + (backward ? expansion.utf16.count : 0), length: 0))
editor.undoManager?.undo()
#expect(editor.string == rendered)
#expect(editor.markdownString == source)
#expect(editor.undoManager?.canUndo == false)
}
}
}
@Test @MainActor
func atomicTasksCopySelectAndPersistMarkdownAlongsideUnicodeAndImages() throws {
_ = NSApplication.shared
@@ -558,11 +681,11 @@ func atomicTasksCopySelectAndPersistMarkdownAlongsideUnicodeAndImages() throws {
#expect(editor.renderedRange(for: (source.string as NSString).range(of: "🦊")) == (rendered as NSString).range(of: "🦊"))
editor.setSelectedRange(NSRange(location: token.location, length: 0))
editor.moveRight(nil)
#expect(editor.selectedRange() == NSRange(location: NSMaxRange(token), length: 0))
#expect(editor.selectedRange() == NSRange(location: NSMaxRange(token) + 1, length: 0))
editor.moveLeft(nil)
#expect(editor.selectedRange() == NSRange(location: token.location, length: 0))
editor.moveRightAndModifySelection(nil)
#expect(editor.selectedRange() == token)
#expect(editor.selectedRange() == NSRange(location: token.location, length: 2))
let board = NSPasteboard.withUniqueName()
defer { board.releaseGlobally() }
editor.setSelectedRange(token)