diff --git a/Package.resolved b/Package.resolved index 4d101036d..9750d78cb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,21 +1,12 @@ { "pins" : [ - { - "identity" : "mainoffender", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mattmassicotte/MainOffender", - "state" : { - "revision" : "343cc3797618c29b48b037b4e2beea0664e75315", - "version" : "0.1.0" - } - }, { "identity" : "rearrange", "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/Rearrange", "state" : { - "revision" : "8f97f721d8a08c6e01ab9f7460e53819bef72dfa", - "version" : "1.5.3" + "revision" : "5ff7f3363f7a08f77e0d761e38e6add31c2136e1", + "version" : "1.8.1" } }, { @@ -23,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" } }, { @@ -32,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/lukepistrol/SwiftLintPlugin", "state" : { - "revision" : "f69b412a765396d44dc9f4788a5b79919c1ca9e3", - "version" : "0.2.2" + "revision" : "5a65f4074975f811da666dfe31a19850950b1ea4", + "version" : "0.56.2" } }, { @@ -41,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/TextStory", "state" : { - "revision" : "8883fa739aa213e70e6cb109bfbf0a0b551e4cb5", - "version" : "0.8.0" + "revision" : "8dc9148b46fcf93b08ea9d4ef9bdb5e4f700e008", + "version" : "0.9.0" } } ], diff --git a/Package.swift b/Package.swift index da4046e50..92feef60c 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( // Text mutation, storage helpers .package( url: "https://github.com/ChimeHQ/TextStory", - from: "0.8.0" + from: "0.9.0" ), // Useful data structures .package( @@ -27,7 +27,7 @@ let package = Package( // SwiftLint .package( url: "https://github.com/lukepistrol/SwiftLintPlugin", - from: "0.2.2" + from: "0.52.2" ) ], targets: [ diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift index 8bd09bd61..d28be05d5 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift @@ -79,6 +79,13 @@ public class TextLayoutManager: NSObject { public var isInTransaction: Bool { transactionCounter > 0 } + #if DEBUG + /// Guard variable for an assertion check in debug builds. + /// Ensures that layout calls are not overlapping, potentially causing layout issues. + /// This is used over a lock, as locks in performant code such as this would be detrimental to performance. + /// Also only included in debug builds. DO NOT USE for checking if layout is active or not. That is an anti-pattern. + private var isInLayout: Bool = false + #endif weak var layoutView: NSView? @@ -188,15 +195,26 @@ public class TextLayoutManager: NSObject { // MARK: - Layout + /// Asserts that the caller is not in an active layout pass. + /// See docs on ``isInLayout`` for more details. + private func assertNotInLayout() { + #if DEBUG // This is redundant, but it keeps the flag debug-only too which helps prevent misuse. + assert(!isInLayout, "layoutLines called while already in a layout pass. This is a programmer error.") + #endif + } + /// Lays out all visible lines func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length + assertNotInLayout() guard layoutView?.superview != nil, let visibleRect = rect ?? delegate?.visibleRect, !isInTransaction, let textStorage else { return } - CATransaction.begin() + #if DEBUG + isInLayout = true + #endif let minY = max(visibleRect.minY - verticalLayoutPadding, 0) let maxY = max(visibleRect.maxY + verticalLayoutPadding, 0) let originalHeight = lineStorage.height @@ -237,13 +255,11 @@ public class TextLayoutManager: NSObject { } } else { // Make sure the used fragment views aren't dequeued. - usedFragmentIDs.formUnion(linePosition.data.typesetter.lineFragments.map(\.data.id)) + usedFragmentIDs.formUnion(linePosition.data.lineFragments.map(\.data.id)) } newVisibleLines.insert(linePosition.data.id) } - CATransaction.commit() - // Enqueue any lines not used in this layout pass. viewReuseQueue.enqueueViews(notInSet: usedFragmentIDs) @@ -262,6 +278,9 @@ public class TextLayoutManager: NSObject { delegate?.layoutManagerYAdjustment(yContentAdjustment) } + #if DEBUG + isInLayout = false + #endif needsLayout = false } @@ -302,7 +321,7 @@ public class TextLayoutManager: NSObject { let relativeMinY = max(layoutData.minY - position.yPos, 0) let relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY) - for lineFragmentPosition in line.typesetter.lineFragments.linesStartingAt( + for lineFragmentPosition in line.lineFragments.linesStartingAt( relativeMinY, until: relativeMaxY ) { diff --git a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift index 6637a0dfe..4578744aa 100644 --- a/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift +++ b/Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift @@ -38,8 +38,8 @@ extension TextView { delegate?.textView(self, didReplaceContentsIn: range, with: string) } - layoutManager.endTransaction() textStorage.endEditing() + layoutManager.endTransaction() selectionManager.notifyAfterEdit() NotificationCenter.default.post(name: Self.textDidChangeNotification, object: self) }