refactor: use structured rich text for Notes editing
This commit is contained in:
@@ -4,7 +4,7 @@ import Testing
|
||||
@testable import Conjure
|
||||
|
||||
@Test @MainActor
|
||||
func headingsFollowVisibleParagraphEditingAndRoundTrip() throws {
|
||||
func headingShortcutsConsumeSyntaxAndPersistAllSixLevels() throws {
|
||||
_ = NSApplication.shared
|
||||
let suite = "NotesHeadings.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suite))
|
||||
@@ -21,90 +21,60 @@ func headingsFollowVisibleParagraphEditingAndRoundTrip() throws {
|
||||
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)
|
||||
func reset(_ source: String) {
|
||||
editor.setMarkdownContents(NSAttributedString(string: source))
|
||||
MarkdownHighlighting.apply(to: editor)
|
||||
editor.refreshTasks()
|
||||
editor.breakUndoCoalescing()
|
||||
editor.undoManager?.removeAllActions()
|
||||
let title = (editor.string as NSString).range(of: "Title")
|
||||
editor.setSelectedRange(NSRange(location: title.location, length: 0))
|
||||
}
|
||||
for (before, after) in [
|
||||
("## Title", "## Title"),
|
||||
("\n## Title", "## Title"),
|
||||
("Text\n## Title", "TextTitle"),
|
||||
("# Text\n## Title", "# TextTitle"),
|
||||
("Text\n\n## Title", "Text\n## Title")
|
||||
] {
|
||||
reset(before)
|
||||
editor.undoManager?.beginUndoGrouping()
|
||||
editor.deleteBackward(nil)
|
||||
editor.undoManager?.endUndoGrouping()
|
||||
#expect(editor.markdownString == after)
|
||||
if before != after {
|
||||
editor.undoManager?.undo()
|
||||
#expect(editor.markdownString == before)
|
||||
editor.undoManager?.redo()
|
||||
#expect(editor.markdownString == after)
|
||||
}
|
||||
}
|
||||
for before in ["\n# Title", "\n## Title", "Text\n\n###### Title", "- [ ] 🦊\n\n## Title"] {
|
||||
reset(before)
|
||||
editor.undoManager?.beginUndoGrouping()
|
||||
editor.doCommand(by: #selector(NSResponder.deleteBackward(_:)))
|
||||
editor.undoManager?.endUndoGrouping()
|
||||
let joined = editor.markdownString
|
||||
editor.breakUndoCoalescing()
|
||||
editor.undoManager?.removeAllActions()
|
||||
editor.undoManager?.beginUndoGrouping()
|
||||
editor.doCommand(by: #selector(NSResponder.insertNewline(_:)))
|
||||
editor.undoManager?.endUndoGrouping()
|
||||
#expect(editor.markdownString == before)
|
||||
#expect(editor.selectedRange() == NSRange(location: (editor.string as NSString).range(of: "Title").location, length: 0))
|
||||
editor.undoManager?.undo()
|
||||
#expect(editor.markdownString == joined)
|
||||
editor.undoManager?.redo()
|
||||
#expect(editor.markdownString == before)
|
||||
#expect(controller.save())
|
||||
#expect(try NotesStore(directory: directory).notes.first?.contents.string == before)
|
||||
}
|
||||
for (before, position, after) in [
|
||||
("## Title", 3, "\n## Title"),
|
||||
("## Title", 5, "## Ti\ntle"),
|
||||
("## Title", 8, "## Title\n"),
|
||||
("Text\n## Title", 4, "Text\n\n## Title"),
|
||||
("```text\n## Title\n```", 11, "```text\n## \nTitle\n```")
|
||||
] {
|
||||
reset(before)
|
||||
editor.setSelectedRange(NSRange(location: position, length: 0))
|
||||
editor.doCommand(by: #selector(NSResponder.insertNewline(_:)))
|
||||
#expect(editor.markdownString == after)
|
||||
}
|
||||
for level in 1...6 {
|
||||
reset("### Title")
|
||||
editor.undoManager?.beginUndoGrouping()
|
||||
editor.setDocumentContents(NSAttributedString(string: "Title"))
|
||||
editor.setSelectedRange(NSRange(location: 0, length: 0))
|
||||
editor.beginInsertionSession()
|
||||
for character in String(repeating: "#", count: level) + " " {
|
||||
editor.insertText(String(character), replacementRange: editor.selectedRange())
|
||||
}
|
||||
editor.undoManager?.endUndoGrouping()
|
||||
let expected = String(repeating: "#", count: level) + " Title"
|
||||
#expect(editor.markdownString == expected)
|
||||
let title = (editor.string as NSString).range(of: "Title")
|
||||
#expect(editor.selectedRange().location == title.location)
|
||||
let sizes: [CGFloat] = [26, 23, 21, 19, 17, 15]
|
||||
#expect((editor.textStorage?.attribute(.font, at: title.location, effectiveRange: nil) as? NSFont)?.pointSize == sizes[level - 1])
|
||||
editor.endInsertionSession()
|
||||
#expect(editor.string == "Title")
|
||||
#expect(editor.selectedRange() == NSRange(location: 0, length: 0))
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.block, at: 0, effectiveRange: nil) as? String == "heading")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.headingLevel, at: 0, effectiveRange: nil) as? Int == level)
|
||||
editor.undoManager?.undo()
|
||||
#expect(editor.markdownString == "### Title")
|
||||
#expect(editor.string == "Title")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.block, at: 0, effectiveRange: nil) == nil)
|
||||
editor.undoManager?.redo()
|
||||
#expect(editor.markdownString == expected)
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.headingLevel, at: 0, effectiveRange: nil) as? Int == level)
|
||||
#expect(controller.save())
|
||||
let reloaded = try #require(NotesStore(directory: directory).notes.first)
|
||||
#expect(reloaded.contents.string == expected)
|
||||
#expect(reloaded.contents.string == "Title")
|
||||
#expect(reloaded.contents.attribute(NoteAttributes.headingLevel, at: 0, effectiveRange: nil) as? Int == level)
|
||||
}
|
||||
reset("```text\n## Title\n```")
|
||||
editor.insertText("# ", replacementRange: editor.selectedRange())
|
||||
#expect(editor.markdownString == "```text\n## # Title\n```")
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func headingReturnSplitsVisibleParagraphAndUndoRestoresItsSemantics() throws {
|
||||
_ = NSApplication.shared
|
||||
let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 600, height: 500))
|
||||
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 position in [2, 5] {
|
||||
editor.setDocumentContents(NoteDocument.importMarkdown(NSAttributedString(string: "## Title")))
|
||||
editor.setSelectedRange(NSRange(location: position, length: 0))
|
||||
editor.undoManager?.removeAllActions()
|
||||
editor.insertNewline(nil)
|
||||
#expect(editor.string == (position == 2 ? "Ti\ntle" : "Title\n"))
|
||||
#expect(editor.selectedRange() == NSRange(location: position + 1, length: 0))
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.headingLevel, at: 0, effectiveRange: nil) as? Int == 2)
|
||||
editor.insertText("body", replacementRange: editor.selectedRange())
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.block, at: position + 1, effectiveRange: nil) == nil)
|
||||
editor.undoManager?.undo()
|
||||
editor.undoManager?.undo()
|
||||
#expect(editor.string == "Title")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.headingLevel, at: 0, effectiveRange: nil) as? Int == 2)
|
||||
}
|
||||
editor.setDocumentContents(NoteDocument.importMarkdown(NSAttributedString(string: "```text\n## Title\n```")))
|
||||
editor.setSelectedRange(NSRange(location: 0, length: 0))
|
||||
for character in "# " { editor.insertText(String(character), replacementRange: editor.selectedRange()) }
|
||||
#expect(editor.string == "# ## Title")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.block, at: 0, effectiveRange: nil) as? String == "code")
|
||||
#expect(editor.documentContents.attribute(NoteAttributes.headingLevel, at: 0, effectiveRange: nil) == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
@@ -116,7 +86,8 @@ func headingCommandsKeepCaretOnTheVisibleBodyLine() throws {
|
||||
window.makeFirstResponder(editor)
|
||||
defer { window.orderOut(nil) }
|
||||
for source in ["\n# Title", "Text\n\n## Title", "Text\n\n\n###### Title"] {
|
||||
editor.setMarkdownContents(NSAttributedString(string: source))
|
||||
editor.setDocumentContents(NoteDocument.importMarkdown(NSAttributedString(string: source)))
|
||||
let before = editor.string
|
||||
editor.setSelectedRange(NSRange(location: (editor.string as NSString).range(of: "Title").location, length: 0))
|
||||
for command in [#selector(NSResponder.deleteBackward(_:)), #selector(NSResponder.insertNewline(_:))] {
|
||||
editor.doCommand(by: command)
|
||||
@@ -128,6 +99,6 @@ func headingCommandsKeepCaretOnTheVisibleBodyLine() throws {
|
||||
#expect(caretRect.origin == titleRect.origin)
|
||||
#expect(caretRect.height == titleRect.height)
|
||||
}
|
||||
#expect(editor.markdownString == source)
|
||||
#expect(editor.string == before)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user