diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachment.swift b/Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachment.swift index f3bc0120..e1f4363d 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachment.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachment.swift @@ -7,11 +7,29 @@ import AppKit +public enum TextAttachmentAction { + /// Perform no action. + case none + /// Replace the attachment range with the given string. + case replace(text: String) + /// Discard the attachment and perform no other action, this is the default action. + case discard +} + /// Represents an attachment type. Attachments take up some set width, and draw their contents in a receiver view. public protocol TextAttachment: AnyObject { var width: CGFloat { get } var isSelected: Bool { get set } + func draw(in context: CGContext, rect: NSRect) + + /// The action that should be performed when this attachment is invoked (double-click, enter pressed). + /// This method is optional, by default the attachment is discarded. + func attachmentAction() -> TextAttachmentAction +} + +public extension TextAttachment { + func attachmentAction() -> TextAttachmentAction { .discard } } /// Type-erasing type for ``TextAttachment`` that also contains range information about the attachment. diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachmentManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachmentManager.swift index 9eb1cde2..7b4d0c0e 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachmentManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachmentManager.swift @@ -102,7 +102,7 @@ public final class TextAttachmentManager { /// - Returns: An array of `AnyTextAttachment` instances whose ranges intersect `query`. public func getAttachmentsOverlapping(_ range: NSRange) -> [AnyTextAttachment] { // Find the first attachment whose end is beyond the start of the query. - guard let startIdx = firstIndex(where: { $0.range.upperBound >= range.location }) else { + guard let startIdx = orderedAttachments.firstIndex(where: { $0.range.upperBound >= range.location }) else { return [] } diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift index 19f67793..ef61dd6e 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift @@ -339,4 +339,12 @@ extension TextLayoutManager { height: lineFragment.scaledHeight ).pixelAligned } + + func contentRun(at offset: Int) -> LineFragment.FragmentContent? { + guard let textLine = textLineForOffset(offset), + let fragment = textLine.data.lineFragments.getLine(atOffset: offset - textLine.range.location) else { + return nil + } + return fragment.data.findContent(at: offset - textLine.range.location - fragment.range.location)?.content + } } diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift index 2bc3d1d4..f0516862 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift @@ -80,7 +80,6 @@ public class TextSelectionManager: NSObject { textSelections = [selection] updateSelectionViews() NotificationCenter.default.post(Notification(name: Self.selectionChangedNotification, object: self)) - delegate?.setNeedsDisplay() } /// Set the selected ranges to new ranges. Overrides any existing selections. diff --git a/Sources/CodeEditTextView/TextView/TextView+Insert.swift b/Sources/CodeEditTextView/TextView/TextView+Insert.swift index 8c4fc408..05bd092a 100644 --- a/Sources/CodeEditTextView/TextView/TextView+Insert.swift +++ b/Sources/CodeEditTextView/TextView/TextView+Insert.swift @@ -9,6 +9,21 @@ import AppKit extension TextView { override public func insertNewline(_ sender: Any?) { + var attachments: [AnyTextAttachment] = selectionManager.textSelections.compactMap({ selection in + let content = layoutManager.contentRun(at: selection.range.location) + if case let .attachment(attachment) = content?.data, attachment.range == selection.range { + return attachment + } + return nil + }) + + if !attachments.isEmpty { + for attachment in attachments.sorted(by: { $0.range.location > $1.range.location }) { + performAttachmentAction(attachment: attachment) + } + return + } + insertText(layoutManager.detectedLineEnding.rawValue) } diff --git a/Sources/CodeEditTextView/TextView/TextView+Mouse.swift b/Sources/CodeEditTextView/TextView/TextView+Mouse.swift index 636ea472..7ab49d79 100644 --- a/Sources/CodeEditTextView/TextView/TextView+Mouse.swift +++ b/Sources/CodeEditTextView/TextView/TextView+Mouse.swift @@ -12,11 +12,17 @@ extension TextView { // Set cursor guard isSelectable, event.type == .leftMouseDown, - let offset = layoutManager.textOffsetAtPoint(self.convert(event.locationInWindow, from: nil)) else { + let offset = layoutManager.textOffsetAtPoint(self.convert(event.locationInWindow, from: nil)), + let content = layoutManager.contentRun(at: offset) else { super.mouseDown(with: event) return } + if case let .attachment(attachment) = content.data, event.clickCount < 3 { + handleAttachmentClick(event: event, offset: offset, attachment: attachment) + return + } + switch event.clickCount { case 1: handleSingleClick(event: event, offset: offset) @@ -76,6 +82,30 @@ extension TextView { selectLine(nil) } + fileprivate func handleAttachmentClick(event: NSEvent, offset: Int, attachment: AnyTextAttachment) { + switch event.clickCount { + case 1: + selectionManager.setSelectedRange(attachment.range) + case 2: + performAttachmentAction(attachment: attachment) + default: + break + } + } + + func performAttachmentAction(attachment: AnyTextAttachment) { + let action = attachment.attachment.attachmentAction() + switch action { + case .none: + return + case .discard: + layoutManager.attachments.remove(atOffset: attachment.range.location) + selectionManager.setSelectedRange(NSRange(location: attachment.range.location, length: 0)) + case let .replace(text): + replaceCharacters(in: attachment.range, with: text) + } + } + override public func mouseUp(with event: NSEvent) { mouseDragAnchor = nil disableMouseAutoscrollTimer()