From f62d59d621368cdd0031dde52582419ea729b553 Mon Sep 17 00:00:00 2001 From: Feror Date: Sat, 30 May 2026 19:28:34 +0200 Subject: [PATCH] perf(editor): apply native edit source to line index --- Sources/SaplingEditor/DocumentLineIndex.swift | 24 +++++++++++++++---- .../SaplingEditor/EditorArchitecture.swift | 8 +++++-- .../EditorPerformanceProfiling.swift | 5 ++-- .../SaplingEditor/HybridMarkdownEditor.swift | 6 ++--- Sources/SaplingEditorBenchmark/main.swift | 18 +++++++------- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/Sources/SaplingEditor/DocumentLineIndex.swift b/Sources/SaplingEditor/DocumentLineIndex.swift index 94e45db..037286c 100644 --- a/Sources/SaplingEditor/DocumentLineIndex.swift +++ b/Sources/SaplingEditor/DocumentLineIndex.swift @@ -156,10 +156,16 @@ public struct DocumentLineIndex: Hashable, Sendable { public mutating func replace(_ edit: DocumentLineIndexEdit) -> DocumentLineIndexEditResult { let oldSource = source as NSString let oldLength = oldSource.length - let range = NSRange( - location: max(0, min(edit.range.location, oldLength)), - length: max(0, min(edit.range.length, oldLength - max(0, min(edit.range.location, oldLength)))) - ) + let range = clampedRange(edit.range, sourceLength: oldLength) + let updatedSource = oldSource.replacingCharacters(in: range, with: edit.replacement) + return replace(edit, updatedSource: updatedSource) + } + + @discardableResult + public mutating func replace(_ edit: DocumentLineIndexEdit, updatedSource: String) -> DocumentLineIndexEditResult { + let oldSource = source as NSString + let oldLength = oldSource.length + let range = clampedRange(edit.range, sourceLength: oldLength) let replacement = edit.replacement let replacementLength = (replacement as NSString).length let locationDelta = replacementLength - range.length @@ -172,7 +178,7 @@ public struct DocumentLineIndex: Hashable, Sendable { let scanStart = boundaries[lowerLineIndex].contentRange.location let oldScanEnd = boundaries[upperLineIndex].nextLineLocation - source = oldSource.replacingCharacters(in: range, with: replacement) + source = updatedSource let newSource = source as NSString let newScanEnd = max(scanStart, min(newSource.length, oldScanEnd + locationDelta)) @@ -202,6 +208,14 @@ public struct DocumentLineIndex: Hashable, Sendable { ) } + private func clampedRange(_ range: NSRange, sourceLength: Int) -> NSRange { + let location = max(0, min(range.location, sourceLength)) + return NSRange( + location: location, + length: max(0, min(range.length, sourceLength - location)) + ) + } + private func editorLine(for boundary: DocumentLineBoundary, activeLineIndex: Int) -> EditorLine { let nsSource = source as NSString return EditorLine( diff --git a/Sources/SaplingEditor/EditorArchitecture.swift b/Sources/SaplingEditor/EditorArchitecture.swift index f500d39..d5a5c2a 100644 --- a/Sources/SaplingEditor/EditorArchitecture.swift +++ b/Sources/SaplingEditor/EditorArchitecture.swift @@ -124,8 +124,12 @@ public struct EditorState: Hashable, Sendable { activeLineIndex = lineIndex.lineIndex(containing: selection.location) } - public mutating func updateSource(_ edit: DocumentLineIndexEdit, selection newSelection: EditorSelection? = nil) { - lineIndex.replace(edit) + public mutating func updateSource( + _ source: String, + edit: DocumentLineIndexEdit, + selection newSelection: EditorSelection? = nil + ) { + lineIndex.replace(edit, updatedSource: source) document.source = lineIndex.source selection = EditorActiveLineTracker.clampedSelection(newSelection ?? selection, in: document.source) activeLineIndex = lineIndex.lineIndex(containing: selection.location) diff --git a/Sources/SaplingEditor/EditorPerformanceProfiling.swift b/Sources/SaplingEditor/EditorPerformanceProfiling.swift index 8c30db4..b2e6f0b 100644 --- a/Sources/SaplingEditor/EditorPerformanceProfiling.swift +++ b/Sources/SaplingEditor/EditorPerformanceProfiling.swift @@ -178,7 +178,7 @@ public enum EditorBenchmarkProfiler { ) let changedSource = sourceByInsertingProbeText(in: source, at: midpoint) var changedLineIndex = lineIndex - changedLineIndex.replace(typingEdit) + changedLineIndex.replace(typingEdit, updatedSource: changedSource) let changedActiveLineIndex = changedLineIndex.lineIndex(containing: midpoint + 1) let activeLineResult = measure { @@ -221,7 +221,8 @@ public enum EditorBenchmarkProfiler { let sourceUpdateResult = measure { var updatedState = state updatedState.updateSource( - typingEdit, + changedSource, + edit: typingEdit, selection: EditorSelection(location: midpoint + typingEdit.replacementUTF16Length, length: 0) ) } diff --git a/Sources/SaplingEditor/HybridMarkdownEditor.swift b/Sources/SaplingEditor/HybridMarkdownEditor.swift index 0825f6e..4adffd5 100644 --- a/Sources/SaplingEditor/HybridMarkdownEditor.swift +++ b/Sources/SaplingEditor/HybridMarkdownEditor.swift @@ -54,7 +54,7 @@ public final class HybridMarkdownEditorViewModel: ObservableObject, EditorCoordi guard state.document.source != source else { return } let previousActiveLineIndex = state.activeLineIndex if let edit { - state.updateSource(edit, selection: selection) + state.updateSource(source, edit: edit, selection: selection) } else { state.updateSource(source) if let selection { @@ -286,7 +286,7 @@ private struct NativeMarkdownTextView: NSViewRepresentable { let selection = EditorSelection(range: textView.selectedRange()) let edit = pendingEdit if let edit { - currentLineIndex.replace(edit) + currentLineIndex.replace(edit, updatedSource: textView.string) } else { currentLineIndex = DocumentLineIndex(source: textView.string) } @@ -515,7 +515,7 @@ private struct NativeMarkdownTextView: UIViewRepresentable { let selection = EditorSelection(range: textView.selectedRange) let edit = pendingEdit if let edit { - currentLineIndex.replace(edit) + currentLineIndex.replace(edit, updatedSource: textView.text) } else { currentLineIndex = DocumentLineIndex(source: textView.text) } diff --git a/Sources/SaplingEditorBenchmark/main.swift b/Sources/SaplingEditorBenchmark/main.swift index 58bef9c..67f11fe 100644 --- a/Sources/SaplingEditorBenchmark/main.swift +++ b/Sources/SaplingEditorBenchmark/main.swift @@ -7,6 +7,15 @@ struct BenchmarkScenario { var url: URL } +let trackedInteractionMetricNames = [ + "active_line_lookup", + "selection_update", + "dirty_line_invalidation_click", + "typing_state_update", + "dirty_line_invalidation_typing", + "render_update_typing_dirty" +] + let arguments = Array(CommandLine.arguments.dropFirst()) let repositoryRoot = URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) @@ -99,15 +108,6 @@ func printScenario(name: String, result: EditorBenchmarkResult) { print("") } -let trackedInteractionMetricNames = [ - "active_line_lookup", - "selection_update", - "dirty_line_invalidation_click", - "typing_state_update", - "dirty_line_invalidation_typing", - "render_update_typing_dirty" -] - func format(_ value: Double) -> String { String(format: "%.3f", value) }