Skip to content
Merged
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 @@ -143,6 +143,8 @@ public struct VSelectableTextView: NSViewRepresentable {
textView.autoresizingMask = [.width]
textView.textContainerInset = .zero

textView.delegate = context.coordinator

textView.linkTextAttributes = [
.foregroundColor: tintColor,
.underlineStyle: NSUnderlineStyle.single.rawValue,
Expand Down Expand Up @@ -193,7 +195,7 @@ public struct VSelectableTextView: NSViewRepresentable {

public func makeCoordinator() -> Coordinator { Coordinator() }

public final class Coordinator {
public final class Coordinator: NSObject, NSTextViewDelegate {
var lastAttributedString: NSAttributedString?
var lastLineSpacing: CGFloat = 0
private var pendingAttributedString: NSAttributedString?
Expand Down Expand Up @@ -240,6 +242,22 @@ public struct VSelectableTextView: NSViewRepresentable {
}
}

// MARK: - NSTextViewDelegate

/// Opens clicked links in the default browser.
/// Reference: https://developer.apple.com/documentation/appkit/nstextviewdelegate/textview(_:clickedonlink:at:)
public func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool {
if let url = link as? URL {
NSWorkspace.shared.open(url)
return true
}
if let string = link as? String, let url = URL(string: string) {
NSWorkspace.shared.open(url)
return true
}
return false
Comment on lines +249 to +258

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Delegate link handler replicates default NSTextView behavior for non-editable text views

For a non-editable NSTextView (isEditable = false), the default behavior already opens links via NSWorkspace.shared.open() when no delegate intercepts the click. The new textView(_:clickedOnLink:at:) at line 249 does exactly what the default handler would do — open the URL in the default browser. This means the explicit delegate is currently a no-op from a behavioral standpoint.

This is likely intentional as a foundation for future customization (e.g., intercepting specific URL schemes, adding analytics, or routing internal links differently). However, if the only goal was to "enable" link clicking, the delegate wasn't needed — links already worked in the non-editable text view. Worth confirming the intent.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The claim that the default NSTextView behavior already opens links for non-editable text views is not accurate in this configuration. The user confirmed links are visually styled but completely inert (LUM-748 screenshot shows pointer cursor and underline but no click response).

Several factors can prevent the default link-opening fallback from firing in practice:

  1. Explicit TextKit 1 stack — this NSTextView is created with a manually constructed TextKit 1 stack (NSTextStorageNSLayoutManagerNSTextContainer), not the default initializer. The default clicked(onLink:at:) fallback behavior may not be wired identically in this path.
  2. isSelectable = true + isEditable = false — in this mode, mouse events are primarily routed through the selection machinery. Link click detection depends on the text view correctly distinguishing a click-on-link from a selection gesture, which can be unreliable without an explicit delegate.
  3. Previous fix attempt — an earlier branch (devin/1774970368-lum-635-selectable-text-view) independently reached the same conclusion and used the delegate approach.

The explicit delegate is the Apple-recommended pattern for handling link clicks (NSTextViewDelegate.textView(_:clickedOnLink:at:)). It provides reliable, deterministic behavior regardless of the TextKit stack configuration, and gives us a hook for future customization (e.g., routing internal links, analytics).

}

func applyAttributedString(
_ attributedString: NSAttributedString,
lineSpacing: CGFloat,
Expand Down