Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+Insert.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
32 changes: 31 additions & 1 deletion Sources/CodeEditTextView/TextView/TextView+Mouse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down