Skip to content

Commit cd2960b

Browse files
committed
Add Actions to Text Attachments
1 parent c1fed34 commit cd2960b

File tree

5 files changed

+54
-3
lines changed

5 files changed

+54
-3
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachment.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,29 @@
77

88
import AppKit
99

10+
public enum TextAttachmentAction {
11+
/// Perform no action.
12+
case none
13+
/// Replace the attachment range with the given string.
14+
case replace(text: String)
15+
/// Discard the attachment and perform no other action, this is the default action.
16+
case discard
17+
}
18+
1019
/// Represents an attachment type. Attachments take up some set width, and draw their contents in a receiver view.
1120
public protocol TextAttachment: AnyObject {
1221
var width: CGFloat { get }
1322
var isSelected: Bool { get set }
23+
1424
func draw(in context: CGContext, rect: NSRect)
25+
26+
/// The action that should be performed when this attachment is invoked (double-click, enter pressed).
27+
/// This method is optional, by default the attachment is discarded.
28+
func attachmentAction() -> TextAttachmentAction
29+
}
30+
31+
public extension TextAttachment {
32+
func attachmentAction() -> TextAttachmentAction { .discard }
1533
}
1634

1735
/// Type-erasing type for ``TextAttachment`` that also contains range information about the attachment.

Sources/CodeEditTextView/TextLayoutManager/TextAttachments/TextAttachmentManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public final class TextAttachmentManager {
102102
/// - Returns: An array of `AnyTextAttachment` instances whose ranges intersect `query`.
103103
public func getAttachmentsOverlapping(_ range: NSRange) -> [AnyTextAttachment] {
104104
// Find the first attachment whose end is beyond the start of the query.
105-
guard let startIdx = firstIndex(where: { $0.range.upperBound >= range.location }) else {
105+
guard let startIdx = orderedAttachments.firstIndex(where: { $0.range.upperBound >= range.location }) else {
106106
return []
107107
}
108108

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Public.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,4 +339,12 @@ extension TextLayoutManager {
339339
height: lineFragment.scaledHeight
340340
).pixelAligned
341341
}
342+
343+
func contentRun(at offset: Int) -> LineFragment.FragmentContent? {
344+
guard let textLine = textLineForOffset(offset),
345+
let fragment = textLine.data.lineFragments.getLine(atOffset: offset - textLine.range.location) else {
346+
return nil
347+
}
348+
return fragment.data.findContent(at: offset - textLine.range.location - fragment.range.location)?.content
349+
}
342350
}

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ public class TextSelectionManager: NSObject {
8080
textSelections = [selection]
8181
updateSelectionViews()
8282
NotificationCenter.default.post(Notification(name: Self.selectionChangedNotification, object: self))
83-
delegate?.setNeedsDisplay()
8483
}
8584

8685
/// Set the selected ranges to new ranges. Overrides any existing selections.

Sources/CodeEditTextView/TextView/TextView+Mouse.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ extension TextView {
1212
// Set cursor
1313
guard isSelectable,
1414
event.type == .leftMouseDown,
15-
let offset = layoutManager.textOffsetAtPoint(self.convert(event.locationInWindow, from: nil)) else {
15+
let offset = layoutManager.textOffsetAtPoint(self.convert(event.locationInWindow, from: nil)),
16+
let content = layoutManager.contentRun(at: offset) else {
1617
super.mouseDown(with: event)
1718
return
1819
}
1920

21+
if case let .attachment(attachment) = content.data, event.clickCount < 3 {
22+
handleAttachmentClick(event: event, offset: offset, attachment: attachment)
23+
return
24+
}
25+
2026
switch event.clickCount {
2127
case 1:
2228
handleSingleClick(event: event, offset: offset)
@@ -76,6 +82,26 @@ extension TextView {
7682
selectLine(nil)
7783
}
7884

85+
fileprivate func handleAttachmentClick(event: NSEvent, offset: Int, attachment: AnyTextAttachment) {
86+
switch event.clickCount {
87+
case 1:
88+
selectionManager.setSelectedRange(attachment.range)
89+
case 2:
90+
let action = attachment.attachment.attachmentAction()
91+
switch action {
92+
case .none:
93+
return
94+
case .discard:
95+
layoutManager.attachments.remove(atOffset: offset)
96+
selectionManager.setSelectedRange(NSRange(location: attachment.range.location, length: 0))
97+
case let .replace(text):
98+
replaceCharacters(in: attachment.range, with: text)
99+
}
100+
default:
101+
break
102+
}
103+
}
104+
79105
override public func mouseUp(with event: NSEvent) {
80106
mouseDragAnchor = nil
81107
disableMouseAutoscrollTimer()

0 commit comments

Comments
 (0)