refactor: use structured rich text for Notes editing
This commit is contained in:
@@ -4,92 +4,98 @@ import Testing
|
||||
@testable import Conjure
|
||||
|
||||
@Test @MainActor
|
||||
func noteFormattingHidesSyntaxButPreservesMarkdownAndLiteralCode() throws {
|
||||
func importedFormattingBecomesVisibleTextAndNativeSemanticMarks() throws {
|
||||
_ = NSApplication.shared
|
||||
let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 600, height: 800))
|
||||
let source = "# Heading\n## Subheading\n*italic* **bold** _emphasis_ __strong__ ~~gone~~ <u>underlined</u> `*literal*`\n> Quote\n[Label](https://example.com)\n```text\n# Code *literal*\n```\nUnmatched * and \\*escaped*"
|
||||
editor.setMarkdownContents(NSAttributedString(string: source))
|
||||
MarkdownHighlighting.apply(to: editor)
|
||||
editor.refreshTasks()
|
||||
let layout = try #require(editor.layoutManager)
|
||||
let container = try #require(editor.textContainer)
|
||||
layout.ensureLayout(for: container)
|
||||
func visibleText() -> String {
|
||||
var result = ""
|
||||
let text = editor.string as NSString
|
||||
for index in 0..<text.length {
|
||||
let glyph = layout.glyphIndexForCharacter(at: index)
|
||||
if !layout.propertyForGlyph(at: glyph).contains(.null) {
|
||||
result += text.substring(with: NSRange(location: index, length: 1))
|
||||
}
|
||||
}
|
||||
return result
|
||||
let source = "# Heading\n## Subheading\n*italic* **bold** _emphasis_ __strong__ ~~gone~~ <u>underlined</u> `*literal*`\n> Quote\n[Label](https://example.com)\n```text\n# Code *literal*\n```"
|
||||
editor.setDocumentContents(NoteDocument.importMarkdown(NSAttributedString(string: source)))
|
||||
#expect(editor.string == "Heading\nSubheading\nitalic bold emphasis strong gone underlined *literal*\nQuote\nLabel\n# Code *literal*")
|
||||
func attribute(_ key: NSAttributedString.Key, in token: String) -> Any? {
|
||||
editor.documentContents.attribute(key, at: (editor.string as NSString).range(of: token).location, effectiveRange: nil)
|
||||
}
|
||||
#expect(visibleText() == "Heading\nSubheading\nitalic bold emphasis strong gone underlined *literal*\nQuote\nLabel\n```text\n# Code *literal*\n```\nUnmatched * and \\*escaped*")
|
||||
#expect(editor.markdownString == source)
|
||||
editor.setSelectedRange(NSRange(location: 0, length: editor.string.utf16.count))
|
||||
let pasteboard = NSPasteboard.withUniqueName()
|
||||
defer { pasteboard.releaseGlobally() }
|
||||
#expect(editor.writeSelection(to: pasteboard, types: editor.writablePasteboardTypes))
|
||||
#expect(pasteboard.string(forType: .string) == source)
|
||||
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
let store = try NotesStore(directory: directory)
|
||||
let id = try store.create()
|
||||
try store.update(id: id, contents: editor.markdownContents)
|
||||
let reloaded = try #require(NotesStore(directory: directory).notes.first)
|
||||
#expect(reloaded.contents.string == source)
|
||||
#expect(attribute(NoteAttributes.headingLevel, in: "Heading") as? Int == 1)
|
||||
#expect(attribute(NoteAttributes.headingLevel, in: "Subheading") as? Int == 2)
|
||||
for token in ["italic", "emphasis"] { #expect(attribute(NoteAttributes.italic, in: token) as? Bool == true) }
|
||||
for token in ["bold", "strong"] { #expect(attribute(NoteAttributes.bold, in: token) as? Bool == true) }
|
||||
#expect(attribute(.strikethroughStyle, in: "gone") as? Int == NSUnderlineStyle.single.rawValue)
|
||||
#expect(attribute(.underlineStyle, in: "underlined") as? Int == NSUnderlineStyle.single.rawValue)
|
||||
#expect(attribute(NoteAttributes.inlineCode, in: "*literal*") as? Bool == true)
|
||||
#expect(attribute(NoteAttributes.block, in: "Quote") as? String == "quote")
|
||||
#expect(String(describing: try #require(attribute(.link, in: "Label"))) == "https://example.com")
|
||||
#expect(attribute(NoteAttributes.block, in: "# Code") as? String == "code")
|
||||
#expect(attribute(NoteAttributes.bold, in: "# Code") as? Bool != true)
|
||||
#expect(attribute(NoteAttributes.headingLevel, in: "# Code") == nil)
|
||||
}
|
||||
|
||||
private final class NotesLayoutProbe: NSLayoutManager {
|
||||
var observeFormatting: ((NSTextStorage) -> Void)?
|
||||
|
||||
override func processEditing(for textStorage: NSTextStorage, edited editMask: NSTextStorageEditActions,
|
||||
range newCharRange: NSRange, changeInLength delta: Int,
|
||||
invalidatedRange invalidatedCharRange: NSRange) {
|
||||
observeFormatting?(textStorage)
|
||||
super.processEditing(for: textStorage, edited: editMask, range: newCharRange,
|
||||
changeInLength: delta, invalidatedRange: invalidatedCharRange)
|
||||
@Test @MainActor
|
||||
func completedInlineShortcutsConsumeDelimitersAndKeepUndoableNativeMarks() throws {
|
||||
_ = NSApplication.shared
|
||||
let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
|
||||
editor.allowsUndo = true
|
||||
let window = NSWindow(contentRect: editor.frame, styleMask: .borderless, backing: .buffered, defer: false)
|
||||
window.contentView = editor
|
||||
window.makeFirstResponder(editor)
|
||||
defer { window.orderOut(nil) }
|
||||
for (source, key) in [("**fox 🦊**", NoteAttributes.bold), ("*fox 🦊*", NoteAttributes.italic),
|
||||
("`fox 🦊`", NoteAttributes.inlineCode), ("~~fox 🦊~~", .strikethroughStyle),
|
||||
("<u>fox 🦊</u>", .underlineStyle), ("[fox 🦊](https://example.com)", .link)] {
|
||||
editor.setDocumentContents(NSAttributedString(string: ""))
|
||||
editor.setSelectedRange(NSRange(location: 0, length: 0))
|
||||
editor.beginInsertionSession()
|
||||
for character in source { editor.insertText(String(character), replacementRange: editor.selectedRange()) }
|
||||
editor.endInsertionSession()
|
||||
#expect(editor.string == "fox 🦊")
|
||||
#expect(editor.selectedRange() == NSRange(location: "fox 🦊".utf16.count, length: 0))
|
||||
let mark = try #require(editor.documentContents.attribute(key, at: 0, effectiveRange: nil))
|
||||
if key == .link {
|
||||
#expect(String(describing: mark) == "https://example.com")
|
||||
} else if key == .underlineStyle || key == .strikethroughStyle {
|
||||
#expect(mark as? Int == NSUnderlineStyle.single.rawValue)
|
||||
} else {
|
||||
#expect(mark as? Bool == true)
|
||||
}
|
||||
editor.undoManager?.undo()
|
||||
#expect(editor.string.isEmpty)
|
||||
editor.undoManager?.redo()
|
||||
#expect(editor.string == "fox 🦊")
|
||||
#expect(editor.documentContents.attribute(key, at: 0, effectiveRange: nil) != nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func typingDoesNotExposeUnformattedExistingTextToLayout() throws {
|
||||
func richCopyPasteKeepsNativeMarksUnicodeAndImageBytesWithoutMarkdownParsing() throws {
|
||||
_ = NSApplication.shared
|
||||
let suite = "NotesFlashing.\(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 store = try NotesStore(directory: directory)
|
||||
let id = try store.create()
|
||||
try store.update(id: id, contents: NSAttributedString(string: "# Heading\n- [ ] Task\nPlain"))
|
||||
defaults.set(id.uuidString, forKey: "selectedNote")
|
||||
let controller = NotesController(defaults: defaults, directory: directory)
|
||||
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 570, height: 404),
|
||||
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)
|
||||
let layout = NotesLayoutProbe()
|
||||
try #require(editor.textContainer).replaceLayoutManager(layout)
|
||||
editor.refreshTasks()
|
||||
window.makeFirstResponder(editor)
|
||||
layout.observeFormatting = { storage in
|
||||
let text = storage.string as NSString
|
||||
let heading = text.range(of: "Heading")
|
||||
let task = text.range(of: "Task")
|
||||
#expect((storage.attribute(.font, at: heading.location, effectiveRange: nil) as? NSFont)?.pointSize == 26)
|
||||
#expect((storage.attribute(.paragraphStyle, at: task.location, effectiveRange: nil) as? NSParagraphStyle)?.headIndent == 22)
|
||||
#expect(storage.attribute(MarkdownHighlighting.hiddenMarkup, at: 0, effectiveRange: nil) as? Bool == true)
|
||||
}
|
||||
defer { layout.observeFormatting = nil }
|
||||
editor.insertText("!", replacementRange: NSRange(location: editor.string.utf16.count, length: 0))
|
||||
#expect(editor.markdownString == "# Heading\n- [ ] Task\nPlain!")
|
||||
#expect(try NotesStore(directory: directory).notes.first?.contents.string == editor.markdownString)
|
||||
let bitmap = try #require(NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: 1, pixelsHigh: 1,
|
||||
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
|
||||
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0))
|
||||
bitmap.setColor(.red, atX: 0, y: 0)
|
||||
let png = try #require(bitmap.representation(using: .png, properties: [:]))
|
||||
let file = FileWrapper(regularFileWithContents: png)
|
||||
file.preferredFilename = "pixel.png"
|
||||
let contents = NSMutableAttributedString(attributedString:
|
||||
NoteDocument.importMarkdown(NSAttributedString(string: "- [x] **🦊 café** ")))
|
||||
contents.append(NSAttributedString(attachment: NSTextAttachment(fileWrapper: file)))
|
||||
let source = NotesTextView(frame: NSRect(x: 0, y: 0, width: 400, height: 200))
|
||||
source.setDocumentContents(contents)
|
||||
source.setSelectedRange(NSRange(location: 0, length: source.string.utf16.count))
|
||||
let board = NSPasteboard.withUniqueName()
|
||||
defer { board.releaseGlobally() }
|
||||
#expect(source.writeSelection(to: board, types: source.writablePasteboardTypes))
|
||||
#expect(board.string(forType: .string) == source.string)
|
||||
let destination = NotesTextView(frame: source.frame)
|
||||
destination.allowsUndo = true
|
||||
#expect(destination.readSelection(from: board))
|
||||
#expect(destination.string == source.string)
|
||||
#expect(destination.documentContents.attribute(NoteAttributes.bold, at: 0, effectiveRange: nil) as? Bool == true)
|
||||
#expect(destination.documentContents.attribute(NoteAttributes.block, at: 0, effectiveRange: nil) as? String == "task")
|
||||
#expect(destination.documentContents.attribute(NoteAttributes.checked, at: 0, effectiveRange: nil) as? Bool == true)
|
||||
let image = try #require(destination.documentContents.attribute(.attachment,
|
||||
at: destination.string.utf16.count - 1, effectiveRange: nil) as? NSTextAttachment)
|
||||
#expect(image.fileWrapper?.regularFileContents == png)
|
||||
destination.undoManager?.undo()
|
||||
#expect(destination.string.isEmpty)
|
||||
destination.undoManager?.redo()
|
||||
#expect(destination.string == source.string)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
@@ -112,7 +118,7 @@ func noteImagesRenderRoundedWithoutChangingSavedImageData() throws {
|
||||
#expect(editor.readSelection(from: pasteboard))
|
||||
|
||||
func checkRoundedDrawing() throws {
|
||||
MarkdownHighlighting.apply(to: editor)
|
||||
editor.applyDocumentStyle()
|
||||
let layout = try #require(editor.layoutManager)
|
||||
layout.ensureLayout(for: try #require(editor.textContainer))
|
||||
let output = try #require(NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: 200, pixelsHigh: 120,
|
||||
@@ -146,60 +152,40 @@ func noteImagesRenderRoundedWithoutChangingSavedImageData() throws {
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
let store = try NotesStore(directory: directory)
|
||||
let id = try store.create()
|
||||
try store.update(id: id, contents: editor.markdownContents)
|
||||
try store.update(id: id, contents: editor.documentContents)
|
||||
let reloaded = try #require(NotesStore(directory: directory).notes.first)
|
||||
let attachment = try #require(reloaded.contents.attribute(.attachment, at: 0, effectiveRange: nil) as? NSTextAttachment)
|
||||
#expect(attachment.fileWrapper?.regularFileContents == png)
|
||||
editor.setMarkdownContents(reloaded.contents)
|
||||
editor.setDocumentContents(reloaded.contents)
|
||||
try checkRoundedDrawing()
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func hiddenNoteMarkupKeepsTypingDeletionFormattingAndUndoAtVisibleBoundaries() throws {
|
||||
func nativeFormattingSurvivesVisibleBoundaryEditingAndUndo() throws {
|
||||
_ = NSApplication.shared
|
||||
let suite = "NotesMarkup.\(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: 570, height: 404),
|
||||
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)
|
||||
let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 600, height: 300))
|
||||
editor.allowsUndo = true
|
||||
let window = NSWindow(contentRect: editor.frame, styleMask: .borderless, backing: .buffered, defer: false)
|
||||
window.contentView = editor
|
||||
window.makeFirstResponder(editor)
|
||||
editor.insertText("# Title\n*word*", replacementRange: NSRange(location: 0, length: 0))
|
||||
editor.setSelectedRange(NSRange(location: 0, length: 0))
|
||||
editor.insertText("A", replacementRange: editor.selectedRange())
|
||||
#expect(editor.markdownString == "# ATitle\n*word*")
|
||||
editor.breakUndoCoalescing()
|
||||
editor.undoManager?.removeAllActions()
|
||||
editor.undoManager?.beginUndoGrouping()
|
||||
defer { window.orderOut(nil) }
|
||||
editor.setDocumentContents(NoteDocument.importMarkdown(NSAttributedString(string: "# Title\n*word*")))
|
||||
editor.insertText("A", replacementRange: NSRange(location: 0, length: 0))
|
||||
#expect(editor.string == "ATitle\nword")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.headingLevel, at: 0, effectiveRange: nil) as? Int == 1)
|
||||
editor.setSelectedRange(NSRange(location: editor.string.utf16.count, length: 0))
|
||||
editor.undoManager?.removeAllActions()
|
||||
editor.deleteBackward(nil)
|
||||
editor.undoManager?.endUndoGrouping()
|
||||
#expect(editor.markdownString == "# ATitle\n*wor*")
|
||||
#expect(editor.string == "ATitle\nwor")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.italic, at: "ATitle\n".utf16.count, effectiveRange: nil) as? Bool == true)
|
||||
editor.undoManager?.undo()
|
||||
#expect(editor.markdownString == "# ATitle\n*word*")
|
||||
#expect(editor.string == "ATitle\nword")
|
||||
let word = (editor.string as NSString).range(of: "word")
|
||||
editor.setSelectedRange(NSRange(location: word.location, length: 0))
|
||||
editor.moveRight(nil)
|
||||
#expect(editor.selectedRange().location == word.location + 1)
|
||||
editor.setSelectedRange(NSRange(location: NSMaxRange(word), length: 0))
|
||||
editor.moveRight(nil)
|
||||
#expect(editor.selectedRange().location == editor.string.utf16.count)
|
||||
editor.setSelectedRange(NSRange(location: word.location, length: 0))
|
||||
let italic = try #require(descendants(controller.view).compactMap { $0 as? NSButton }.first { $0.accessibilityLabel() == "Italic" })
|
||||
// Selecting the body and toggling formatting must remove its hidden delimiters.
|
||||
editor.setSelectedRange(word)
|
||||
italic.performClick(nil)
|
||||
#expect(editor.markdownString == "# ATitle\nword")
|
||||
#expect(controller.save())
|
||||
#expect(try NotesStore(directory: directory).notes.first?.contents.string == "# ATitle\nword")
|
||||
editor.format(2)
|
||||
#expect(editor.string == "ATitle\nword")
|
||||
#expect(editor.selectedRange() == word)
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.italic, at: word.location, effectiveRange: nil) as? Bool != true)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
@@ -234,62 +220,51 @@ func notesFormattingShortcutsToggleCombinedStylesAndRespectPopupFocus() throws {
|
||||
}
|
||||
editor.insertText("fox 🦊", replacementRange: editor.selectedRange())
|
||||
editor.setSelectedRange(NSRange(location: 0, length: editor.string.utf16.count))
|
||||
let selection = editor.selectedRange()
|
||||
#expect(try shortcut("b"))
|
||||
#expect(editor.markdownString == "**fox 🦊**")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.bold, at: 0, effectiveRange: nil) as? Bool == true)
|
||||
#expect(try shortcut("i"))
|
||||
#expect(editor.markdownString == "***fox 🦊***")
|
||||
let font = try #require(editor.textStorage?.attribute(.font, at: editor.selectedRange().location,
|
||||
effectiveRange: nil) as? NSFont)
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.italic, at: 0, effectiveRange: nil) as? Bool == true)
|
||||
let font = try #require(editor.textStorage?.attribute(.font, at: 0, effectiveRange: nil) as? NSFont)
|
||||
let traits = NSFontManager.shared.traits(of: font)
|
||||
#expect(traits.contains(.boldFontMask) && traits.contains(.italicFontMask))
|
||||
#expect(try shortcut("i"))
|
||||
#expect(editor.markdownString == "**fox 🦊**")
|
||||
#expect(try shortcut("b"))
|
||||
#expect(editor.markdownString == "fox 🦊")
|
||||
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.bold, at: 0, effectiveRange: nil) as? Bool != true)
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.italic, at: 0, effectiveRange: nil) as? Bool != true)
|
||||
#expect(try shortcut("s", shift: true))
|
||||
#expect(editor.markdownString == "~~fox 🦊~~")
|
||||
#expect(editor.textStorage?.attribute(.strikethroughStyle, at: editor.selectedRange().location,
|
||||
effectiveRange: nil) as? Int == NSUnderlineStyle.single.rawValue)
|
||||
#expect(editor.documentContents.attribute(.strikethroughStyle, at: 0, effectiveRange: nil) as? Int == NSUnderlineStyle.single.rawValue)
|
||||
#expect(try shortcut("s", shift: true))
|
||||
#expect(editor.markdownString == "fox 🦊")
|
||||
editor.breakUndoCoalescing()
|
||||
#expect(editor.documentContents.attribute(.strikethroughStyle, at: 0, effectiveRange: nil) == nil)
|
||||
editor.undoManager?.removeAllActions()
|
||||
editor.undoManager?.beginUndoGrouping()
|
||||
#expect(try shortcut("u", shift: true))
|
||||
editor.undoManager?.endUndoGrouping()
|
||||
#expect(editor.markdownString == "<u>fox 🦊</u>")
|
||||
#expect(editor.textStorage?.attribute(.underlineStyle, at: editor.selectedRange().location,
|
||||
effectiveRange: nil) as? Int == NSUnderlineStyle.single.rawValue)
|
||||
#expect(editor.documentContents.attribute(.underlineStyle, at: 0, effectiveRange: nil) as? Int == NSUnderlineStyle.single.rawValue)
|
||||
#expect(editor.string == "fox 🦊")
|
||||
#expect(editor.selectedRange() == selection)
|
||||
#expect(controller.save())
|
||||
#expect(try NotesStore(directory: directory).notes.first?.contents.string == "<u>fox 🦊</u>")
|
||||
#expect(try NoteMarkdownExport(contents: editor.markdownContents).clipboardText == "<u>fox 🦊</u>")
|
||||
let saved = try #require(NotesStore(directory: directory).notes.first)
|
||||
#expect(saved.contents.string == "fox 🦊")
|
||||
#expect(saved.contents.attribute(.underlineStyle, at: 0, effectiveRange: nil) as? Int == NSUnderlineStyle.single.rawValue)
|
||||
#expect(try NoteMarkdownExport(contents: editor.documentContents).clipboardText == "<u>fox 🦊</u>")
|
||||
editor.undoManager?.undo()
|
||||
#expect(editor.markdownString == "fox 🦊")
|
||||
#expect(try shortcut("u", shift: true))
|
||||
#expect(try shortcut("u", shift: true))
|
||||
#expect(editor.markdownString == "fox 🦊")
|
||||
#expect(editor.textStorage?.attribute(.underlineStyle, at: 0, effectiveRange: nil) == nil)
|
||||
#expect(editor.documentContents.attribute(.underlineStyle, at: 0, effectiveRange: nil) == nil)
|
||||
#expect(try shortcut("l", shift: true))
|
||||
#expect(editor.markdownString == "- fox 🦊")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.block, at: 0, effectiveRange: nil) as? String == "bullet")
|
||||
#expect(try shortcut("l", shift: true))
|
||||
#expect(editor.markdownString == "fox 🦊")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.block, at: 0, effectiveRange: nil) == nil)
|
||||
#expect(!(try shortcut("b", option: true)))
|
||||
#expect(editor.markdownString == "fox 🦊")
|
||||
|
||||
editor.breakUndoCoalescing()
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.bold, at: 0, effectiveRange: nil) as? Bool != true)
|
||||
editor.undoManager?.removeAllActions()
|
||||
editor.undoManager?.beginUndoGrouping()
|
||||
#expect(try shortcut("e"))
|
||||
editor.undoManager?.endUndoGrouping()
|
||||
#expect(editor.markdownString == "`fox 🦊`")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.inlineCode, at: 0, effectiveRange: nil) as? Bool == true)
|
||||
editor.undoManager?.undo()
|
||||
#expect(editor.markdownString == "fox 🦊")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.inlineCode, at: 0, effectiveRange: nil) as? Bool != true)
|
||||
#expect(try shortcut("k"))
|
||||
let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first { $0.accessibilityLabel() == "Search notes actions" })
|
||||
#expect(window.firstResponder !== editor)
|
||||
_ = try shortcut("b")
|
||||
#expect(editor.markdownString == "fox 🦊")
|
||||
#expect(editor.string == "fox 🦊")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.bold, at: 0, effectiveRange: nil) as? Bool != true)
|
||||
#expect(search.stringValue.isEmpty)
|
||||
#expect(controller.handle(#selector(NSResponder.cancelOperation(_:))))
|
||||
#expect(controller.save())
|
||||
@@ -297,30 +272,28 @@ func notesFormattingShortcutsToggleCombinedStylesAndRespectPopupFocus() throws {
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func inlineCodeContainerStaysWithinCodeAndDisappearsWhenDelimitersAreRemoved() throws {
|
||||
func inlineCodeContainerIsLimitedToItsMarkAndDisappearsWhenToggledOff() throws {
|
||||
_ = NSApplication.shared
|
||||
let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 200, height: 300))
|
||||
let source = "Before `some *literal* code 🦊` after\n```text\n`not inline` <u>literal</u>\n```"
|
||||
editor.setMarkdownContents(NSAttributedString(string: source))
|
||||
editor.setDocumentContents(NoteDocument.importMarkdown(NSAttributedString(string: source)))
|
||||
let storage = try #require(editor.textStorage)
|
||||
let layout = try #require(editor.layoutManager)
|
||||
layout.ensureLayout(for: try #require(editor.textContainer))
|
||||
let body = (source as NSString).range(of: "some *literal* code 🦊")
|
||||
let body = (editor.string as NSString).range(of: "some *literal* code 🦊")
|
||||
for index in body.location..<NSMaxRange(body) {
|
||||
#expect(storage.attribute(.foregroundColor, at: index, effectiveRange: nil) as? NSColor == SunsetPalette.accent)
|
||||
#expect(storage.attribute(.backgroundColor, at: index, effectiveRange: nil) as? NSColor == SunsetPalette.selectionFill)
|
||||
}
|
||||
for token in ["Before", "after", "not inline"] {
|
||||
let index = (source as NSString).range(of: token).location
|
||||
#expect(storage.attribute(.backgroundColor, at: index, effectiveRange: nil) == nil)
|
||||
let index = (editor.string as NSString).range(of: token).location
|
||||
#expect(storage.attribute(NoteAttributes.inlineCode, at: index, effectiveRange: nil) as? Bool != true)
|
||||
}
|
||||
let literalUnderline = (source as NSString).range(of: "<u>literal</u>")
|
||||
let literalUnderline = (editor.string as NSString).range(of: "<u>literal</u>")
|
||||
#expect(storage.attribute(.underlineStyle, at: literalUnderline.location, effectiveRange: nil) == nil)
|
||||
#expect(editor.markdownString == source)
|
||||
#expect(editor.markdownContents.attribute(.backgroundColor, at: body.location, effectiveRange: nil) == nil)
|
||||
editor.replaceMarkdown("some *literal* code 🦊",
|
||||
range: NSRange(location: body.location - 1, length: body.length + 2))
|
||||
let plain = (editor.string as NSString).range(of: "some")
|
||||
#expect(storage.attribute(.backgroundColor, at: plain.location, effectiveRange: nil) == nil)
|
||||
#expect(storage.attribute(.foregroundColor, at: plain.location, effectiveRange: nil) as? NSColor == SunsetPalette.text)
|
||||
let before = editor.string
|
||||
editor.setSelectedRange(body)
|
||||
editor.format(4)
|
||||
#expect(editor.string == before)
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.inlineCode, at: body.location, effectiveRange: nil) as? Bool != true)
|
||||
#expect(storage.attribute(.backgroundColor, at: body.location, effectiveRange: nil) == nil)
|
||||
#expect(storage.attribute(.foregroundColor, at: body.location, effectiveRange: nil) as? NSColor == SunsetPalette.text)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user