From bc405e44b5d56403a63264331e18885983f58cf6 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Tue, 27 May 2025 12:10:52 -0500 Subject: [PATCH 1/2] Update Build Documentation Workflow An attempt to fix the build documentation workflow that's failing on main. --- .github/workflows/build-documentation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-documentation.yml b/.github/workflows/build-documentation.yml index 3b73dfdd5..63756f779 100644 --- a/.github/workflows/build-documentation.yml +++ b/.github/workflows/build-documentation.yml @@ -7,7 +7,7 @@ jobs: runs-on: [self-hosted, macOS] steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build Documentation run: exec ./.github/scripts/build-docc.sh - name: Init new repo in dist folder and commit generated files @@ -20,7 +20,7 @@ jobs: git commit -m 'deploy' - name: Force push to destination branch - uses: ad-m/github-push-action@v0.6.0 + uses: ad-m/github-push-action@v0.8.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: docs From 69282e2ea7ad8976b062b945d575da47b61ed208 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Wed, 28 May 2025 10:09:30 -0500 Subject: [PATCH 2/2] [hotfix:] Layout After New/Removed Lines (#97) ### Description Hotfix to fix mistakenly broken editing capabilities for inserting or deleting new lines. Updates the layout pass to correctly detect new lines, and if a new one is laid out, continues to layout lines after that line. Eg: ``` 1 2 <- Insert "\n" 3 ``` Before: ``` [visible text] 1 2 <- Layout invalidated 3 <- Layout _NOT_ invalidated, missing new line. ``` Now: ``` [visible text] 1 2 <- Layout invalidated <- Layout invalidated 3 <- Layout invalidated ``` Adds a new test case for this. ### Related Issues ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots --- .../TextLayoutManager+Layout.swift | 18 ++++++++----- .../TextLayoutManagerTests.swift | 25 ++++++++++++++++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift index cdbc2704d..042622830 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager+Layout.swift @@ -87,30 +87,28 @@ extension TextLayoutManager { #if DEBUG var laidOutLines: Set = [] #endif - // Layout all lines, fetching lines lazily as they are laid out. for linePosition in linesStartingAt(minY, until: maxY).lazy { guard linePosition.yPos < maxY else { continue } // Three ways to determine if a line needs to be re-calculated. - let changedWidth = linePosition.data.needsLayout(maxWidth: maxLineLayoutWidth) + let linePositionNeedsLayout = linePosition.data.needsLayout(maxWidth: maxLineLayoutWidth) let wasNotVisible = !visibleLineIds.contains(linePosition.data.id) let lineNotEntirelyLaidOut = linePosition.height != linePosition.data.lineFragments.height - if forceLayout || changedWidth || wasNotVisible || lineNotEntirelyLaidOut { + if forceLayout || linePositionNeedsLayout || wasNotVisible || lineNotEntirelyLaidOut { let lineSize = layoutLine( linePosition, textStorage: textStorage, layoutData: LineLayoutData(minY: minY, maxY: maxY, maxWidth: maxLineLayoutWidth), laidOutFragmentIDs: &usedFragmentIDs ) - if lineSize.height != linePosition.height { + let wasLineHeightChanged = lineSize.height != linePosition.height + if wasLineHeightChanged { lineStorage.update( atOffset: linePosition.range.location, delta: 0, deltaHeight: lineSize.height - linePosition.height ) - // If we've updated a line's height, force re-layout for the rest of the pass. - forceLayout = true if linePosition.yPos < minY { // Adjust the scroll position by the difference between the new height and old. @@ -123,6 +121,14 @@ extension TextLayoutManager { #if DEBUG laidOutLines.insert(linePosition.data.id) #endif + // If we've updated a line's height, or a line position was newly laid out, force re-layout for the + // rest of the pass (going down the screen). + // + // These two signals identify: + // - New lines being inserted & Lines being deleted (lineNotEntirelyLaidOut) + // - Line updated for width change (wasLineHeightChanged) + + forceLayout = forceLayout || wasLineHeightChanged || lineNotEntirelyLaidOut } else { // Make sure the used fragment views aren't dequeued. usedFragmentIDs.formUnion(linePosition.data.lineFragments.map(\.data.id)) diff --git a/Tests/CodeEditTextViewTests/LayoutManager/TextLayoutManagerTests.swift b/Tests/CodeEditTextViewTests/LayoutManager/TextLayoutManagerTests.swift index dc34b6e25..7306e62dd 100644 --- a/Tests/CodeEditTextViewTests/LayoutManager/TextLayoutManagerTests.swift +++ b/Tests/CodeEditTextViewTests/LayoutManager/TextLayoutManagerTests.swift @@ -187,6 +187,9 @@ struct TextLayoutManagerTests { #expect(layoutManager.needsLayout == false) } + /// Invalidating a range shouldn't cause a layout on any other lines next layout pass. + /// Note that this is correct behavior, and edits that add or remove lines will trigger another heuristic. + /// See `editsWithNewlinesForceLayoutGoingDownScreen` @Test func invalidatingRangeLaysOutLines() { layoutManager.layoutLines(in: NSRect(x: 0, y: 0, width: 1000, height: 1000)) @@ -203,6 +206,26 @@ struct TextLayoutManagerTests { let invalidatedLineIds = layoutManager.layoutLines() - #expect(invalidatedLineIds == lineIds, "Invalidated lines != lines that were laid out in next pass.") + #expect( + invalidatedLineIds.isSuperset(of: lineIds), + "Invalidated lines != lines that were laid out in next pass." + ) + } + + /// Inserting a new line should cause layout going down the rest of the screen, because the following lines + /// should have moved their position to accomodate the new line. + @Test + func editsWithNewlinesForceLayoutGoingDownScreen() { + layoutManager.layoutLines(in: NSRect(x: 0, y: 0, width: 1000, height: 1000)) + textStorage.replaceCharacters(in: NSRange(start: 4, end: 4), with: "Z\n") + + let expectedLineIds = Array( + layoutManager.lineStorage.linesInRange(NSRange(location: 4, length: 9)) + ).map { $0.data.id } + + #expect(layoutManager.needsLayout == false) // No forced layout for entire view + + let invalidatedLineIds = layoutManager.layoutLines() + #expect(Set(expectedLineIds) == invalidatedLineIds) } }