From 3d90a43175572a2743edb0ea1e63f551c826c8e6 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 12 Jun 2026 17:13:40 +0300 Subject: [PATCH] Wrap selected text in a markdown link when pasting a URL over it Pasting a single link on top of a selection in the message composer now produces a markdown link, e.g. selecting "Google" and pasting "google.com" yields "[Google](google.com)". Non-link clipboard contents and pastes without a selection keep their existing behaviour. --- .../View/MessageComposerTextField.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift index 48396bd22..75b6b1ca1 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift @@ -283,6 +283,13 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate { } override func paste(_ sender: Any?) { + // When pasting a link over a selection, wrap the selection in a markdown link. + if selectedRange.length > 0, let link = UIPasteboard.general.pastedLink { + let selectedText = (attributedText.string as NSString).substring(with: selectedRange) + insertText("[\(selectedText)](\(link))") + return + } + let providers = UIPasteboard.general.itemProviders // Use the default behavior if there are any unsupported providers @@ -331,6 +338,25 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate { } } +private extension UIPasteboard { + /// The pasteboard's string contents when they consist of a single link and nothing else. + var pastedLink: String? { + guard let string = string?.trimmingCharacters(in: .whitespacesAndNewlines), !string.isEmpty, + let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { + return nil + } + + let range = NSRange(string.startIndex..., in: string) + let matches = detector.matches(in: string, range: range) + + guard matches.count == 1, matches[0].range == range else { + return nil + } + + return string + } +} + struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview { static var previews: some View { VStack(spacing: 16) {