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) {
|
public func updateSource(_ source: String) {
|
||||||
|
guard state.document.source != source else { return }
|
||||||
state.updateSource(source)
|
state.updateSource(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateSelection(_ selection: EditorSelection) {
|
public func updateSelection(_ selection: EditorSelection) {
|
||||||
|
guard state.selection != selection else { return }
|
||||||
state.updateSelection(selection)
|
state.updateSelection(selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,13 +174,15 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
||||||
guard let textView = scrollView.documentView as? NSTextView else { return }
|
guard let textView = scrollView.documentView as? NSTextView else { return }
|
||||||
|
|
||||||
if textView.string != text {
|
if textView.string != text {
|
||||||
textView.string = text
|
context.coordinator.performProgrammaticUpdate {
|
||||||
|
textView.string = text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedRange = selection.range
|
let selectedRange = selection.range
|
||||||
if textView.selectedRange() != selectedRange,
|
if textView.selectedRange() != selectedRange,
|
||||||
selectedRange.location <= textView.string.utf16.count {
|
selectedRange.location <= textView.string.utf16.count {
|
||||||
textView.setSelectedRange(selectedRange)
|
context.coordinator.setSelection(selectedRange, in: textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
context.coordinator.applyHybridAttributes(to: textView)
|
context.coordinator.applyHybridAttributes(to: textView)
|
||||||
|
|
@ -186,43 +190,79 @@ private struct NativeMarkdownTextView: NSViewRepresentable {
|
||||||
|
|
||||||
final class Coordinator: NSObject, NSTextViewDelegate {
|
final class Coordinator: NSObject, NSTextViewDelegate {
|
||||||
var parent: NativeMarkdownTextView
|
var parent: NativeMarkdownTextView
|
||||||
private var isApplyingAttributes = false
|
private var programmaticUpdateDepth = 0
|
||||||
|
private var lastStyledText: String?
|
||||||
|
private var lastStyledActiveLineIndex: Int?
|
||||||
|
|
||||||
init(_ parent: NativeMarkdownTextView) {
|
init(_ parent: NativeMarkdownTextView) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
func textDidChange(_ notification: Notification) {
|
func textDidChange(_ notification: Notification) {
|
||||||
guard !isApplyingAttributes,
|
guard !isPerformingProgrammaticUpdate else { return }
|
||||||
let textView = notification.object as? NSTextView
|
guard let textView = notification.object as? NSTextView else { return }
|
||||||
else { return }
|
|
||||||
|
|
||||||
parent.text = textView.string
|
parent.text = textView.string
|
||||||
parent.selection = EditorSelection(range: textView.selectedRange())
|
parent.selection = EditorSelection(range: textView.selectedRange())
|
||||||
|
lastStyledText = nil
|
||||||
applyHybridAttributes(to: textView)
|
applyHybridAttributes(to: textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func textViewDidChangeSelection(_ notification: Notification) {
|
func textViewDidChangeSelection(_ notification: Notification) {
|
||||||
|
guard !isPerformingProgrammaticUpdate else { return }
|
||||||
guard let textView = notification.object as? NSTextView 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)
|
applyHybridAttributes(to: textView)
|
||||||
|
parent.selection = newSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyHybridAttributes(to textView: NSTextView) {
|
func applyHybridAttributes(to textView: NSTextView) {
|
||||||
guard let textStorage = textView.textStorage else { return }
|
guard let textStorage = textView.textStorage else { return }
|
||||||
isApplyingAttributes = true
|
guard shouldRestyle(textView.string) else { return }
|
||||||
|
|
||||||
let selectedRange = textView.selectedRange()
|
let selectedRange = textView.selectedRange()
|
||||||
MarkdownTextStyler.apply(
|
performProgrammaticUpdate {
|
||||||
to: textStorage,
|
MarkdownTextStyler.apply(
|
||||||
activeLineIndex: parent.activeLineIndex,
|
to: textStorage,
|
||||||
backgroundColor: .textBackgroundColor,
|
activeLineIndex: parent.activeLineIndex,
|
||||||
activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
|
backgroundColor: .textBackgroundColor,
|
||||||
textColor: .labelColor,
|
activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
|
||||||
secondaryTextColor: .secondaryLabelColor,
|
textColor: .labelColor,
|
||||||
accentColor: .controlAccentColor
|
secondaryTextColor: .secondaryLabelColor,
|
||||||
)
|
accentColor: .controlAccentColor
|
||||||
textView.setSelectedRange(selectedRange)
|
)
|
||||||
isApplyingAttributes = false
|
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) {
|
func updateUIView(_ textView: UITextView, context: Context) {
|
||||||
context.coordinator.parent = self
|
context.coordinator.parent = self
|
||||||
if textView.text != text {
|
if textView.text != text {
|
||||||
textView.text = text
|
context.coordinator.performProgrammaticUpdate {
|
||||||
|
textView.text = text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
context.coordinator.applyHybridAttributes(to: textView)
|
context.coordinator.applyHybridAttributes(to: textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Coordinator: NSObject, UITextViewDelegate {
|
final class Coordinator: NSObject, UITextViewDelegate {
|
||||||
var parent: NativeMarkdownTextView
|
var parent: NativeMarkdownTextView
|
||||||
private var isApplyingAttributes = false
|
private var programmaticUpdateDepth = 0
|
||||||
|
private var lastStyledText: String?
|
||||||
|
private var lastStyledActiveLineIndex: Int?
|
||||||
|
|
||||||
init(_ parent: NativeMarkdownTextView) {
|
init(_ parent: NativeMarkdownTextView) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
guard !isApplyingAttributes else { return }
|
guard !isPerformingProgrammaticUpdate else { return }
|
||||||
parent.text = textView.text
|
parent.text = textView.text
|
||||||
parent.selection = EditorSelection(range: textView.selectedRange)
|
parent.selection = EditorSelection(range: textView.selectedRange)
|
||||||
|
lastStyledText = nil
|
||||||
applyHybridAttributes(to: textView)
|
applyHybridAttributes(to: textView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func textViewDidChangeSelection(_ textView: UITextView) {
|
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)
|
applyHybridAttributes(to: textView)
|
||||||
|
parent.selection = newSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyHybridAttributes(to textView: UITextView) {
|
func applyHybridAttributes(to textView: UITextView) {
|
||||||
isApplyingAttributes = true
|
guard shouldRestyle(textView.text) else { return }
|
||||||
|
|
||||||
let selectedRange = textView.selectedRange
|
let selectedRange = textView.selectedRange
|
||||||
MarkdownTextStyler.apply(
|
performProgrammaticUpdate {
|
||||||
to: textView.textStorage,
|
MarkdownTextStyler.apply(
|
||||||
activeLineIndex: parent.activeLineIndex,
|
to: textView.textStorage,
|
||||||
backgroundColor: .systemBackground,
|
activeLineIndex: parent.activeLineIndex,
|
||||||
activeLineBackgroundColor: .systemBlue.withAlphaComponent(0.10),
|
backgroundColor: .systemBackground,
|
||||||
textColor: .label,
|
activeLineBackgroundColor: .systemBlue.withAlphaComponent(0.10),
|
||||||
secondaryTextColor: .secondaryLabel,
|
textColor: .label,
|
||||||
accentColor: .systemBlue
|
secondaryTextColor: .secondaryLabel,
|
||||||
)
|
accentColor: .systemBlue
|
||||||
textView.selectedRange = selectedRange
|
)
|
||||||
isApplyingAttributes = false
|
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