fix(editor): prevent recursive selection updates
This commit is contained in:
parent
527a99c2e7
commit
5e1b7ad885
1 changed files with 107 additions and 35 deletions
|
|
@ -37,10 +37,12 @@ public final class HybridMarkdownEditorViewModel: ObservableObject, EditorCoordi
|
|||
}
|
||||
|
||||
public func updateSource(_ source: String) {
|
||||
guard state.document.source != source else { return }
|
||||
state.updateSource(source)
|
||||
}
|
||||
|
||||
public func updateSelection(_ selection: EditorSelection) {
|
||||
guard state.selection != selection else { return }
|
||||
state.updateSelection(selection)
|
||||
}
|
||||
|
||||
|
|
@ -172,13 +174,15 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
|||
guard let textView = scrollView.documentView as? NSTextView else { return }
|
||||
|
||||
if textView.string != text {
|
||||
textView.string = text
|
||||
context.coordinator.performProgrammaticUpdate {
|
||||
textView.string = text
|
||||
}
|
||||
}
|
||||
|
||||
let selectedRange = selection.range
|
||||
if textView.selectedRange() != selectedRange,
|
||||
selectedRange.location <= textView.string.utf16.count {
|
||||
textView.setSelectedRange(selectedRange)
|
||||
context.coordinator.setSelection(selectedRange, in: textView)
|
||||
}
|
||||
|
||||
context.coordinator.applyHybridAttributes(to: textView)
|
||||
|
|
@ -186,43 +190,79 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
|||
|
||||
final class Coordinator: NSObject, NSTextViewDelegate {
|
||||
var parent: NativeMarkdownTextView
|
||||
private var isApplyingAttributes = false
|
||||
private var programmaticUpdateDepth = 0
|
||||
private var lastStyledText: String?
|
||||
private var lastStyledActiveLineIndex: Int?
|
||||
|
||||
init(_ parent: NativeMarkdownTextView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func textDidChange(_ notification: Notification) {
|
||||
guard !isApplyingAttributes,
|
||||
let textView = notification.object as? NSTextView
|
||||
else { return }
|
||||
guard !isPerformingProgrammaticUpdate else { return }
|
||||
guard let textView = notification.object as? NSTextView else { return }
|
||||
|
||||
parent.text = textView.string
|
||||
parent.selection = EditorSelection(range: textView.selectedRange())
|
||||
lastStyledText = nil
|
||||
applyHybridAttributes(to: textView)
|
||||
}
|
||||
|
||||
func textViewDidChangeSelection(_ notification: Notification) {
|
||||
guard !isPerformingProgrammaticUpdate else { return }
|
||||
guard let textView = notification.object as? NSTextView else { return }
|
||||
parent.selection = EditorSelection(range: textView.selectedRange())
|
||||
let newSelection = EditorSelection(range: textView.selectedRange())
|
||||
guard parent.selection != newSelection else { return }
|
||||
applyHybridAttributes(to: textView)
|
||||
parent.selection = newSelection
|
||||
}
|
||||
|
||||
func applyHybridAttributes(to textView: NSTextView) {
|
||||
guard let textStorage = textView.textStorage else { return }
|
||||
isApplyingAttributes = true
|
||||
guard shouldRestyle(textView.string) else { return }
|
||||
|
||||
let selectedRange = textView.selectedRange()
|
||||
MarkdownTextStyler.apply(
|
||||
to: textStorage,
|
||||
activeLineIndex: parent.activeLineIndex,
|
||||
backgroundColor: .textBackgroundColor,
|
||||
activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
|
||||
textColor: .labelColor,
|
||||
secondaryTextColor: .secondaryLabelColor,
|
||||
accentColor: .controlAccentColor
|
||||
)
|
||||
textView.setSelectedRange(selectedRange)
|
||||
isApplyingAttributes = false
|
||||
performProgrammaticUpdate {
|
||||
MarkdownTextStyler.apply(
|
||||
to: textStorage,
|
||||
activeLineIndex: parent.activeLineIndex,
|
||||
backgroundColor: .textBackgroundColor,
|
||||
activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
|
||||
textColor: .labelColor,
|
||||
secondaryTextColor: .secondaryLabelColor,
|
||||
accentColor: .controlAccentColor
|
||||
)
|
||||
if textView.selectedRange() != selectedRange,
|
||||
selectedRange.location <= textView.string.utf16.count {
|
||||
textView.setSelectedRange(selectedRange)
|
||||
}
|
||||
}
|
||||
|
||||
lastStyledText = textView.string
|
||||
lastStyledActiveLineIndex = parent.activeLineIndex
|
||||
}
|
||||
|
||||
func setSelection(_ range: NSRange, in textView: NSTextView) {
|
||||
guard textView.selectedRange() != range else { return }
|
||||
performProgrammaticUpdate {
|
||||
textView.setSelectedRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
func performProgrammaticUpdate(_ updates: () -> Void) {
|
||||
programmaticUpdateDepth += 1
|
||||
defer {
|
||||
programmaticUpdateDepth -= 1
|
||||
}
|
||||
updates()
|
||||
}
|
||||
|
||||
private var isPerformingProgrammaticUpdate: Bool {
|
||||
programmaticUpdateDepth > 0
|
||||
}
|
||||
|
||||
private func shouldRestyle(_ text: String) -> Bool {
|
||||
lastStyledText != text || lastStyledActiveLineIndex != parent.activeLineIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -254,45 +294,77 @@ private struct NativeMarkdownTextView: UIViewRepresentable {
|
|||
func updateUIView(_ textView: UITextView, context: Context) {
|
||||
context.coordinator.parent = self
|
||||
if textView.text != text {
|
||||
textView.text = text
|
||||
context.coordinator.performProgrammaticUpdate {
|
||||
textView.text = text
|
||||
}
|
||||
}
|
||||
context.coordinator.applyHybridAttributes(to: textView)
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, UITextViewDelegate {
|
||||
var parent: NativeMarkdownTextView
|
||||
private var isApplyingAttributes = false
|
||||
private var programmaticUpdateDepth = 0
|
||||
private var lastStyledText: String?
|
||||
private var lastStyledActiveLineIndex: Int?
|
||||
|
||||
init(_ parent: NativeMarkdownTextView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
guard !isApplyingAttributes else { return }
|
||||
guard !isPerformingProgrammaticUpdate else { return }
|
||||
parent.text = textView.text
|
||||
parent.selection = EditorSelection(range: textView.selectedRange)
|
||||
lastStyledText = nil
|
||||
applyHybridAttributes(to: textView)
|
||||
}
|
||||
|
||||
func textViewDidChangeSelection(_ textView: UITextView) {
|
||||
parent.selection = EditorSelection(range: textView.selectedRange)
|
||||
guard !isPerformingProgrammaticUpdate else { return }
|
||||
let newSelection = EditorSelection(range: textView.selectedRange)
|
||||
guard parent.selection != newSelection else { return }
|
||||
applyHybridAttributes(to: textView)
|
||||
parent.selection = newSelection
|
||||
}
|
||||
|
||||
func applyHybridAttributes(to textView: UITextView) {
|
||||
isApplyingAttributes = true
|
||||
guard shouldRestyle(textView.text) else { return }
|
||||
|
||||
let selectedRange = textView.selectedRange
|
||||
MarkdownTextStyler.apply(
|
||||
to: textView.textStorage,
|
||||
activeLineIndex: parent.activeLineIndex,
|
||||
backgroundColor: .systemBackground,
|
||||
activeLineBackgroundColor: .systemBlue.withAlphaComponent(0.10),
|
||||
textColor: .label,
|
||||
secondaryTextColor: .secondaryLabel,
|
||||
accentColor: .systemBlue
|
||||
)
|
||||
textView.selectedRange = selectedRange
|
||||
isApplyingAttributes = false
|
||||
performProgrammaticUpdate {
|
||||
MarkdownTextStyler.apply(
|
||||
to: textView.textStorage,
|
||||
activeLineIndex: parent.activeLineIndex,
|
||||
backgroundColor: .systemBackground,
|
||||
activeLineBackgroundColor: .systemBlue.withAlphaComponent(0.10),
|
||||
textColor: .label,
|
||||
secondaryTextColor: .secondaryLabel,
|
||||
accentColor: .systemBlue
|
||||
)
|
||||
if textView.selectedRange != selectedRange,
|
||||
selectedRange.location <= textView.text.utf16.count {
|
||||
textView.selectedRange = selectedRange
|
||||
}
|
||||
}
|
||||
|
||||
lastStyledText = textView.text
|
||||
lastStyledActiveLineIndex = parent.activeLineIndex
|
||||
}
|
||||
|
||||
func performProgrammaticUpdate(_ updates: () -> Void) {
|
||||
programmaticUpdateDepth += 1
|
||||
defer {
|
||||
programmaticUpdateDepth -= 1
|
||||
}
|
||||
updates()
|
||||
}
|
||||
|
||||
private var isPerformingProgrammaticUpdate: Bool {
|
||||
programmaticUpdateDepth > 0
|
||||
}
|
||||
|
||||
private func shouldRestyle(_ text: String) -> Bool {
|
||||
lastStyledText != text || lastStyledActiveLineIndex != parent.activeLineIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue