fix: insert list body text after hidden separators
This commit is contained in:
@@ -147,6 +147,8 @@ List markers are atomic inline tokens, including nested bullets and numbers; tas
|
||||
|
||||
Unordered markers display as `•`; numbered markers display their original number and delimiter, right-aligned before the item text. The stored and copied Markdown retains the original bullet spelling.
|
||||
|
||||
Typing, pasting, and Return at the visible start of an item use the position after its preserved Markdown separator. Continuing to type after a marker converts therefore keeps the item rendered, rather than inserting characters into the marker syntax.
|
||||
|
||||
Fenced code uses [HighlighterSwift](https://github.com/smittytone/HighlighterSwift) (Highlight.js) for language-aware token colors. Add a language after the opening backticks or tildes, for example `py`, `python`, `swift`, `js`, or `json`. Unlabeled fences use automatic detection; unknown languages remain plain monospace. Highlighting runs entirely offline.
|
||||
|
||||
Notes autosave locally as RTFD packages, including embedded images, under `~/Library/Application Support/Conjure/Notes`. No account, network access, or cloud sync is involved. Save failures appear below the editor and prevent switching, closing, or quitting until saving succeeds, preserving the unsaved buffer. The Back chevron restores the launcher search when no popup is open. Command-W dismisses Conjure, including any open popup; leaving Notes or switching destinations removes its popup completely.
|
||||
|
||||
@@ -145,7 +145,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
guard textView === editor, editor.isEditable,
|
||||
commandSelector == #selector(NSResponder.insertNewline(_:)) else { return false }
|
||||
let source = editor.markdownString as NSString
|
||||
let selection = editor.markdownRange(for: editor.selectedRange())
|
||||
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))
|
||||
|
||||
@@ -202,6 +202,12 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
replaceSource(NSAttributedString(string: text, attributes: textTypingAttributes), range: range, semantic: true)
|
||||
}
|
||||
|
||||
func markdownInsertionRange(for rendered: NSRange) -> NSRange {
|
||||
guard rendered.length == 0 else { return markdownRange(for: rendered) }
|
||||
let body = listWhitespaceBoundary(from: rendered.location, forward: true)
|
||||
return markdownRange(for: NSRange(location: body, length: 0))
|
||||
}
|
||||
|
||||
override func insertText(_ insertString: Any, replacementRange: NSRange) {
|
||||
let replacement: NSAttributedString
|
||||
if let attributed = insertString as? NSAttributedString {
|
||||
@@ -210,7 +216,7 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
replacement = NSAttributedString(string: text, attributes: textTypingAttributes)
|
||||
} else { return }
|
||||
let range = replacementRange.location == NSNotFound ? selectedRange() : replacementRange
|
||||
replaceSource(replacement, range: markdownRange(for: range), semantic: false)
|
||||
replaceSource(replacement, range: markdownInsertionRange(for: range), semantic: false)
|
||||
}
|
||||
|
||||
private func replaceSource(_ replacement: NSAttributedString, range: NSRange, semantic: Bool) {
|
||||
@@ -252,7 +258,8 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
if semantic { breakUndoCoalescing() }
|
||||
}
|
||||
refreshTasks()
|
||||
setSelectedRange(renderedRange(for: NSRange(location: range.location + replacement.length, length: 0)))
|
||||
let caret = renderedRange(for: NSRange(location: range.location + replacement.length, length: 0))
|
||||
setSelectedRange(NSRange(location: listWhitespaceBoundary(from: caret.location, forward: true), length: 0))
|
||||
}
|
||||
|
||||
override func deleteBackward(_ sender: Any?) {
|
||||
@@ -434,7 +441,7 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
reader.importsGraphics = importsGraphics
|
||||
guard reader.readSelection(from: pboard, type: type) else { return false }
|
||||
replaceSource(Self.serialize(reader.attributedString(), keepingMetadata: true),
|
||||
range: markdownRange(for: selectedRange()), semantic: true)
|
||||
range: markdownInsertionRange(for: selectedRange()), semantic: true)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -979,3 +979,62 @@ func renderedListMarkersPaintBulletsAndNumbersInsideTheirGlyphFrames() throws {
|
||||
#expect(editor.markdownString == source)
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func listBodyInsertionUsesVisiblePositionForTypingPasteAndReplacementRanges() throws {
|
||||
_ = NSApplication.shared
|
||||
let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 520, height: 200))
|
||||
editor.isRichText = true
|
||||
editor.allowsUndo = true
|
||||
let window = NSWindow(contentRect: editor.frame, styleMask: .borderless, backing: .buffered, defer: false)
|
||||
window.contentView = editor
|
||||
defer { window.orderOut(nil) }
|
||||
window.makeFirstResponder(editor)
|
||||
let board = NSPasteboard.withUniqueName()
|
||||
defer { board.releaseGlobally() }
|
||||
board.setString("pasted", forType: .string)
|
||||
for marker in ["-", "*", "+", "1.", "123)", "- []", "- [ ]", "- [X]", "\t12) []"] {
|
||||
let separator = " \t "
|
||||
let original = marker + separator + "tail"
|
||||
for offset in 0...separator.utf16.count {
|
||||
for explicitRange in [false, true] {
|
||||
editor.setMarkdownContents(NSAttributedString(string: original))
|
||||
let token = (editor.string as NSString).range(of: "\u{fffc}")
|
||||
let insertion = NSRange(location: NSMaxRange(token) + offset, length: 0)
|
||||
editor.setSelectedRange(insertion)
|
||||
editor.breakUndoCoalescing()
|
||||
editor.undoManager?.removeAllActions()
|
||||
editor.insertText("🦊", replacementRange: explicitRange ? insertion : NSRange(location: NSNotFound, length: 0))
|
||||
#expect(editor.markdownString == marker + separator + "🦊tail")
|
||||
#expect((editor.string as NSString).range(of: "\u{fffc}").location != NSNotFound)
|
||||
#expect(editor.selectedRange().location == NSMaxRange(token) + separator.utf16.count + 2)
|
||||
editor.breakUndoCoalescing()
|
||||
editor.undoManager?.undo()
|
||||
#expect(editor.markdownString == original)
|
||||
editor.undoManager?.redo()
|
||||
#expect(editor.markdownString == marker + separator + "🦊tail")
|
||||
}
|
||||
editor.setMarkdownContents(NSAttributedString(string: original))
|
||||
let token = (editor.string as NSString).range(of: "\u{fffc}")
|
||||
editor.setSelectedRange(NSRange(location: NSMaxRange(token) + offset, length: 0))
|
||||
#expect(editor.readSelection(from: board))
|
||||
#expect(editor.markdownString == marker + separator + "pastedtail")
|
||||
}
|
||||
editor.setMarkdownContents(NSAttributedString(string: ""))
|
||||
editor.setSelectedRange(NSRange(location: 0, length: 0))
|
||||
for character in marker + " " {
|
||||
editor.insertText(String(character), replacementRange: editor.selectedRange())
|
||||
}
|
||||
let token = (editor.string as NSString).range(of: "\u{fffc}")
|
||||
#expect(editor.selectedRange().location == NSMaxRange(token) + 1)
|
||||
editor.insertText("text", replacementRange: NSRange(location: NSNotFound, length: 0))
|
||||
#expect(editor.markdownString == marker + " text")
|
||||
}
|
||||
editor.setMarkdownContents(NSAttributedString(string: "- ab🦊cd"))
|
||||
let emoji = (editor.string as NSString).range(of: "🦊")
|
||||
editor.insertText("X", replacementRange: emoji)
|
||||
#expect(editor.markdownString == "- abXcd")
|
||||
let separator = NSRange(location: 1, length: 3)
|
||||
editor.insertText("!", replacementRange: separator)
|
||||
#expect(editor.markdownString == "-!abXcd")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user