diff --git a/CHANGELOG.md b/CHANGELOG.md index efd68d7..1438712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `MarkdownEditorConfiguration.safeAreaInsets`. ### Fixed +- `NativeTextViewWrapper` keeps links clickable and text selectable + when `isEditable: false`; `isSelectable` is no longer coupled to + `isEditable`. (#31) - `NativeTextViewWrapper` now applies its initial styling pass even when the bound text starts at its final value (e.g. supplied as a SwiftUI `@State` initializer). Previously the editor would render the raw diff --git a/Sources/MarkdownEngine/Parser/MarkdownDetection.swift b/Sources/MarkdownEngine/Parser/MarkdownDetection.swift index df252bd..ca76043 100644 --- a/Sources/MarkdownEngine/Parser/MarkdownDetection.swift +++ b/Sources/MarkdownEngine/Parser/MarkdownDetection.swift @@ -16,8 +16,13 @@ enum MarkdownDetection { static func computeActiveTokenIndices( selectionRange: NSRange, tokens: [MarkdownToken], - in text: NSString + in text: NSString, + suppressed: Bool = false ) -> Set { + // In read-only mode (no caret) we never want to reveal raw markdown + // syntax — `isEditable: false` should hide all tokens regardless of + // the trailing selection NSTextView keeps around after a click. + if suppressed { return [] } var indices: Set = [] let caretLocation = selectionRange.location for (index, token) in tokens.enumerated() { diff --git a/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+Restyling.swift b/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+Restyling.swift index 8b56f11..003eb95 100644 --- a/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+Restyling.swift +++ b/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+Restyling.swift @@ -46,11 +46,15 @@ extension NativeTextViewCoordinator { textView.textStorage?.setAttributes(baseAttrs, range: fullRange) let tokens = parsedDocument(for: displayText).tokens - let caretLocation = textView.selectedRange().location + // Hide the caret position from styling when the view is read-only, + // otherwise clicks reveal raw token syntax (#, `, etc.) even though + // the user can't edit. + let caretLocation = textView.isEditable ? textView.selectedRange().location : -1 activeTokenIndices = MarkdownDetection.computeActiveTokenIndices( selectionRange: textView.selectedRange(), tokens: tokens, - in: nsDisplay + in: nsDisplay, + suppressed: !textView.isEditable ) let ranges = MarkdownStyler.styleAttributes( @@ -102,7 +106,7 @@ extension NativeTextViewCoordinator { paragraphCandidates: paragraphCandidates, baseFont: baseFont, paragraphStyle: paragraphStyle, - caretLocation: textView.selectedRange().location, + caretLocation: textView.isEditable ? textView.selectedRange().location : -1, activeTokenIndices: activeTokenIndices, wikiLinkIDProvider: { [weak self] range in self?.wikiLinkID(for: range) @@ -217,7 +221,8 @@ extension NativeTextViewCoordinator { activeTokenIndices = MarkdownDetection.computeActiveTokenIndices( selectionRange: textView.selectedRange(), tokens: tokens, - in: nsText + in: nsText, + suppressed: !textView.isEditable ) restyleTextView(textView, paragraphCandidates: paragraphs, tokens: tokens) } diff --git a/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+TextDelegate.swift b/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+TextDelegate.swift index 5a68e32..31935a6 100644 --- a/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+TextDelegate.swift +++ b/Sources/MarkdownEngine/TextView/Coordinator/NativeTextViewCoordinator+TextDelegate.swift @@ -118,7 +118,8 @@ extension NativeTextViewCoordinator { activeTokenIndices = MarkdownDetection.computeActiveTokenIndices( selectionRange: safeSelRange, tokens: tokens, - in: fullText + in: fullText, + suppressed: !tv.isEditable ) filterImageEmbedActiveTokens(parsed: parsed, text: fullText, selectionLocation: safeSelRange.location) updateAutocorrectSettings( @@ -182,7 +183,7 @@ extension NativeTextViewCoordinator { let nsText = tv.string as NSString let prevActive = activeTokenIndices - activeTokenIndices = MarkdownDetection.computeActiveTokenIndices(selectionRange: selRange, tokens: tokens, in: nsText) + activeTokenIndices = MarkdownDetection.computeActiveTokenIndices(selectionRange: selRange, tokens: tokens, in: nsText, suppressed: !tv.isEditable) filterImageEmbedActiveTokens(parsed: parsed, text: nsText, selectionLocation: selRange.location) updateAutocorrectSettings( tv, @@ -323,7 +324,8 @@ extension NativeTextViewCoordinator { pendingPreEditActiveTokenIndices = MarkdownDetection.computeActiveTokenIndices( selectionRange: textView.selectedRange(), tokens: parsed.tokens, - in: textView.string as NSString + in: textView.string as NSString, + suppressed: !textView.isEditable ) // Block LaTeX auto-wrap: insert newlines to keep $$ on its own line diff --git a/Sources/MarkdownEngine/TextView/NativeTextViewWrapper.swift b/Sources/MarkdownEngine/TextView/NativeTextViewWrapper.swift index 0f99802..841c8f3 100644 --- a/Sources/MarkdownEngine/TextView/NativeTextViewWrapper.swift +++ b/Sources/MarkdownEngine/TextView/NativeTextViewWrapper.swift @@ -140,7 +140,7 @@ public struct NativeTextViewWrapper: NSViewRepresentable { context.coordinator.configuration = configuration textView.insertionPointColor = configuration.theme.bodyText textView.isEditable = isEditable - textView.isSelectable = isEditable + textView.isSelectable = true textView.isRichText = true let initialState = WikiLinkService.makeDisplayState(from: text) textView.string = initialState.display @@ -264,7 +264,7 @@ public struct NativeTextViewWrapper: NSViewRepresentable { (nsView.documentView as? NativeTextView)?.configuration.services = configuration.services } textView.isEditable = isEditable - textView.isSelectable = isEditable + textView.isSelectable = true textView.insertionPointColor = isEditable ? context.coordinator.configuration.theme.bodyText : .clear let fontChanged = (context.coordinator.fontName != fontName) || (context.coordinator.fontSize != fontSize) if let pendingInlineReplacement {