import AppKit import ConjureKit import Testing @testable import Conjure @Test @MainActor 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~~ underlined `*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(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) } @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), ("fox 🦊", .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 richCopyPasteKeepsNativeMarksUnicodeAndImageBytesWithoutMarkdownParsing() throws { _ = NSApplication.shared 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 func noteImagesRenderRoundedWithoutChangingSavedImageData() throws { _ = NSApplication.shared let image = NSImage(size: NSSize(width: 80, height: 48), flipped: false) { rect in NSColor.red.setFill() rect.fill() return true } let tiff = try #require(image.tiffRepresentation) let bitmap = try #require(NSBitmapImageRep(data: tiff)) let png = try #require(bitmap.representation(using: .png, properties: [:])) let pasteboard = NSPasteboard.withUniqueName() defer { pasteboard.releaseGlobally() } pasteboard.setData(png, forType: .png) let editor = NotesTextView(frame: NSRect(x: 0, y: 0, width: 200, height: 120)) editor.isRichText = true editor.importsGraphics = true #expect(editor.readSelection(from: pasteboard)) func checkRoundedDrawing() throws { 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, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)) let context = try #require(NSGraphicsContext(bitmapImageRep: output)) NSGraphicsContext.saveGraphicsState() defer { NSGraphicsContext.restoreGraphicsState() } NSGraphicsContext.current = context context.cgContext.clear(CGRect(x: 0, y: 0, width: 200, height: 120)) layout.drawGlyphs(forGlyphRange: layout.glyphRange(forCharacterRange: NSRange(location: 0, length: 1), actualCharacterRange: nil), at: .zero) var left = 200, right = -1, top = 120, bottom = -1 for y in 0..<120 { for x in 0..<200 where (output.colorAt(x: x, y: y)?.alphaComponent ?? 0) > 0.5 { left = min(left, x); right = max(right, x) top = min(top, y); bottom = max(bottom, y) } } try #require(right > left && bottom > top) for x in [left, right] { for y in [top, bottom] { #expect(try #require(output.colorAt(x: x, y: y)).alphaComponent < 0.1) } } let center = try #require(output.colorAt(x: (left + right) / 2, y: (top + bottom) / 2)?.usingColorSpace(.deviceRGB)) #expect(center.redComponent > 0.9 && center.greenComponent < 0.1 && center.alphaComponent > 0.9) } try checkRoundedDrawing() 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.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.setDocumentContents(reloaded.contents) try checkRoundedDrawing() } @Test @MainActor func nativeFormattingSurvivesVisibleBoundaryEditingAndUndo() 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) } 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) #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.string == "ATitle\nword") let word = (editor.string as NSString).range(of: "word") editor.setSelectedRange(word) 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 func notesFormattingShortcutsToggleCombinedStylesAndRespectPopupFocus() throws { _ = NSApplication.shared let suite = "NotesFormattingKeys.\(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 launcher = LauncherWindowController( history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }), defaults: defaults, notesDirectory: directory) let window = try #require(launcher.window) defer { launcher.dismiss(restoringFocus: false) } launcher.present(command: .notes) let content = try #require(window.contentView) func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) } let editor = try #require(descendants(content).compactMap { $0 as? NotesTextView }.first) let controller = try #require(editor.delegate as? NotesController) func shortcut(_ key: String, shift: Bool = false, option: Bool = false) throws -> Bool { var flags: NSEvent.ModifierFlags = .command if shift { flags.insert(.shift) } if option { flags.insert(.option) } let event = try #require(NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: flags, timestamp: 1, windowNumber: window.windowNumber, context: nil, characters: shift ? key.uppercased() : key, charactersIgnoringModifiers: shift ? key.uppercased() : key, isARepeat: false, keyCode: 0)) return window.performKeyEquivalent(with: event) } 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.documentContents.attribute(NoteAttributes.bold, at: 0, effectiveRange: nil) as? Bool == true) #expect(try shortcut("i")) #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(try shortcut("b")) #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.documentContents.attribute(.strikethroughStyle, at: 0, effectiveRange: nil) as? Int == NSUnderlineStyle.single.rawValue) #expect(try shortcut("s", shift: true)) #expect(editor.documentContents.attribute(.strikethroughStyle, at: 0, effectiveRange: nil) == nil) editor.undoManager?.removeAllActions() #expect(try shortcut("u", shift: true)) #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()) 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 == "fox 🦊") editor.undoManager?.undo() #expect(editor.documentContents.attribute(.underlineStyle, at: 0, effectiveRange: nil) == nil) #expect(try shortcut("l", shift: true)) #expect(editor.documentContents.attribute(NoteAttributes.block, at: 0, effectiveRange: nil) as? String == "bullet") #expect(try shortcut("l", shift: true)) #expect(editor.documentContents.attribute(NoteAttributes.block, at: 0, effectiveRange: nil) == nil) #expect(!(try shortcut("b", option: true))) #expect(editor.documentContents.attribute(NoteAttributes.bold, at: 0, effectiveRange: nil) as? Bool != true) editor.undoManager?.removeAllActions() #expect(try shortcut("e")) #expect(editor.documentContents.attribute(NoteAttributes.inlineCode, at: 0, effectiveRange: nil) as? Bool == true) editor.undoManager?.undo() #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.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()) #expect(try NotesStore(directory: directory).notes.first?.contents.string == "fox 🦊") } @Test @MainActor 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` literal\n```" editor.setDocumentContents(NoteDocument.importMarkdown(NSAttributedString(string: source))) let storage = try #require(editor.textStorage) let body = (editor.string as NSString).range(of: "some *literal* code 🦊") for index in body.location..literal") #expect(storage.attribute(.underlineStyle, at: literalUnderline.location, effectiveRange: nil) == nil) 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) }