feat: hide rendered markdown formatting markers
This commit is contained in:
@@ -61,6 +61,7 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
private static func serialize(_ contents: NSAttributedString, keepingMetadata: Bool = false) -> NSAttributedString {
|
||||
let result = NSMutableAttributedString(attributedString: contents)
|
||||
restoreListLayout(in: result)
|
||||
result.removeAttribute(MarkdownHighlighting.hiddenMarkup, range: NSRange(location: 0, length: result.length))
|
||||
var replacements: [(NSRange, ListAttachment)] = []
|
||||
contents.enumerateAttribute(.attachment, in: NSRange(location: 0, length: contents.length)) { value, range, _ in
|
||||
if let task = value as? ListAttachment { replacements.append((range, task)) }
|
||||
@@ -195,6 +196,7 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
attributes[.paragraphStyle] = original as? NSParagraphStyle
|
||||
}
|
||||
attributes.removeValue(forKey: listWhitespace)
|
||||
attributes.removeValue(forKey: MarkdownHighlighting.hiddenMarkup)
|
||||
return attributes
|
||||
}
|
||||
|
||||
@@ -208,7 +210,7 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
|
||||
func markdownInsertionRange(for rendered: NSRange) -> NSRange {
|
||||
guard rendered.length == 0 else { return markdownRange(for: rendered) }
|
||||
let body = listWhitespaceBoundary(from: rendered.location, forward: true)
|
||||
let body = hiddenBoundary(from: rendered.location, forward: true)
|
||||
return markdownRange(for: NSRange(location: body, length: 0))
|
||||
}
|
||||
|
||||
@@ -297,30 +299,35 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
}
|
||||
refreshTasks()
|
||||
let caret = renderedRange(for: NSRange(location: range.location + replacement.length, length: 0))
|
||||
setSelectedRange(NSRange(location: listWhitespaceBoundary(from: caret.location, forward: true), length: 0))
|
||||
setSelectedRange(NSRange(location: hiddenBoundary(from: caret.location, forward: true), length: 0))
|
||||
}
|
||||
|
||||
override func deleteBackward(_ sender: Any?) {
|
||||
let selection = selectedRange()
|
||||
let boundary = listWhitespaceBoundary(from: selection.location, forward: false)
|
||||
let boundary = hiddenBoundary(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))
|
||||
} else if selection.location > 0 {
|
||||
let range = (string as NSString).rangeOfComposedCharacterSequence(at: selection.location - 1)
|
||||
let source = string as NSString
|
||||
let atLineStart = boundary == 0 || source.character(at: boundary - 1) == 10
|
||||
let range = boundary < selection.location && atLineStart
|
||||
? NSRange(location: boundary, length: selection.location - boundary)
|
||||
: source.rangeOfComposedCharacterSequence(at: max(0, boundary - 1))
|
||||
replaceMarkdown("", range: markdownRange(for: range))
|
||||
}
|
||||
}
|
||||
|
||||
override func deleteForward(_ sender: Any?) {
|
||||
let selection = selectedRange()
|
||||
let boundary = hiddenBoundary(from: selection.location, forward: true)
|
||||
if selection.length == 0, let task = tasks.first(where: { $0.range.location == selection.location }) {
|
||||
expand(task, backwards: false)
|
||||
} else if selection.length > 0 {
|
||||
replaceMarkdown("", range: markdownRange(for: selection))
|
||||
} else if selection.location < string.utf16.count {
|
||||
let range = (string as NSString).rangeOfComposedCharacterSequence(at: selection.location)
|
||||
} else if boundary < string.utf16.count {
|
||||
let range = (string as NSString).rangeOfComposedCharacterSequence(at: boundary)
|
||||
replaceMarkdown("", range: markdownRange(for: range))
|
||||
}
|
||||
}
|
||||
@@ -335,24 +342,25 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
if !backwards { setSelectedRange(NSRange(location: task.range.location, length: 0)) }
|
||||
}
|
||||
|
||||
// Only separator glyphs are null; indentation and body whitespace remain native stops.
|
||||
private func listWhitespaceBoundary(from position: Int, forward: Bool) -> Int {
|
||||
// Skip display-only syntax without changing the source offsets used by copy and save.
|
||||
private func hiddenBoundary(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
|
||||
func boundary(for key: NSAttributedString.Key) -> Int? {
|
||||
var range = NSRange()
|
||||
guard storage.attribute(key, at: index, effectiveRange: &range) as? Bool == true else { return nil }
|
||||
return forward ? NSMaxRange(range) : range.location
|
||||
}
|
||||
return forward ? NSMaxRange(range) : range.location
|
||||
return boundary(for: listWhitespace) ?? boundary(for: MarkdownHighlighting.hiddenMarkup) ?? position
|
||||
}
|
||||
|
||||
private func visualListBoundary(from position: Int, forward: Bool) -> Int? {
|
||||
let boundary = listWhitespaceBoundary(from: position, forward: forward)
|
||||
let boundary = hiddenBoundary(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
|
||||
return forward ? hiddenBoundary(from: NSMaxRange(task.range), forward: true) : task.range.location
|
||||
}
|
||||
|
||||
override func setSelectedRange(_ charRange: NSRange) {
|
||||
@@ -412,9 +420,9 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
|
||||
// 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)
|
||||
let origin = hiddenBoundary(from: head, forward: true)
|
||||
if let nativeHead, origin == head {
|
||||
let target = listWhitespaceBoundary(from: nativeHead, forward: true)
|
||||
let target = hiddenBoundary(from: nativeHead, forward: true)
|
||||
if target != nativeHead { setHorizontalSelection(anchor: anchor, head: target) }
|
||||
return
|
||||
}
|
||||
@@ -424,7 +432,7 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
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)
|
||||
let target = hiddenBoundary(from: newHead, forward: true)
|
||||
if selecting {
|
||||
if tracked != nil || nativeHead != nil || target != newHead || origin != head {
|
||||
setHorizontalSelection(anchor: anchor, head: target)
|
||||
@@ -579,13 +587,15 @@ final class NotesTextView: NSTextView, @MainActor NSLayoutManagerDelegate {
|
||||
properties props: UnsafePointer<NSLayoutManager.GlyphProperty>,
|
||||
characterIndexes charIndexes: UnsafePointer<Int>, font aFont: NSFont,
|
||||
forGlyphRange glyphRange: NSRange) -> Int {
|
||||
guard let storage = layoutManager.textStorage,
|
||||
(0..<glyphRange.length).contains(where: {
|
||||
storage.attribute(listWhitespace, at: charIndexes[$0], effectiveRange: nil) as? Bool == true
|
||||
}) else { return 0 }
|
||||
guard let storage = layoutManager.textStorage else { return 0 }
|
||||
func hidden(_ index: Int) -> Bool {
|
||||
storage.attribute(listWhitespace, at: index, effectiveRange: nil) as? Bool == true
|
||||
|| storage.attribute(MarkdownHighlighting.hiddenMarkup, at: index, effectiveRange: nil) as? Bool == true
|
||||
}
|
||||
guard (0..<glyphRange.length).contains(where: { hidden(charIndexes[$0]) }) else { return 0 }
|
||||
var properties = Array(UnsafeBufferPointer(start: props, count: glyphRange.length))
|
||||
for index in properties.indices {
|
||||
if storage.attribute(listWhitespace, at: charIndexes[index], effectiveRange: nil) as? Bool == true {
|
||||
if hidden(charIndexes[index]) {
|
||||
properties[index] = .null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user