From ebada4e3787b2bb9fff7193fdad8960efe3873d2 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 30 May 2025 10:52:51 -0500 Subject: [PATCH 1/2] Fix Overlapping Lines Method --- .../TextViewController+IndentLines.swift | 34 ++++++++++++++++--- .../Controller/TextViewControllerTests.swift | 26 ++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+IndentLines.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+IndentLines.swift index 9ef797219..64207d6d8 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+IndentLines.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+IndentLines.swift @@ -67,7 +67,7 @@ extension TextViewController { var selectionIndex = 0 textView.editSelections { textView, selection in // get lineindex, i.e line-numbers+1 - guard let lineIndexes = getHighlightedLines(for: selection.range) else { return } + guard let lineIndexes = getOverlappingLines(for: selection.range) else { return } adjustIndentation(lineIndexes: lineIndexes, inwards: inwards) @@ -128,8 +128,25 @@ extension TextViewController { } return false } - - private func getHighlightedLines(for range: NSRange) -> ClosedRange? { + + /// Find the range of lines overlapping a text range. + /// + /// Use this method to determine what lines to apply a text transformation on using a text selection. For instance, + /// when indenting a selected line. + /// + /// Does not determine the *visible* lines, which is a very slight change from most + /// ``CodeEditTextView/TextLayoutManager`` APIs. + /// Given the text: + /// ``` + /// A + /// B + /// ``` + /// This method will return lines `0...0` for the text range `0..<2`. The layout manager might return lines + /// `0...1`, as the text range contains the newline, which appears *visually* in line index `1`. + /// + /// - Parameter range: The text range in the document to find contained lines for. + /// - Returns: A closed range of line indexes (0-indexed) where each line is overlapping the given text range. + func getOverlappingLines(for range: NSRange) -> ClosedRange? { guard let startLineInfo = textView.layoutManager.textLineForOffset(range.lowerBound) else { return nil } @@ -139,7 +156,16 @@ extension TextViewController { return startLineInfo.index...startLineInfo.index } - return startLineInfo.index...endLineInfo.index + // If we've selected up to the start of a line (just over the newline character), the layout manager tells us + // we've selected the next line. However, we aren't overlapping the *text line* with that range, so we + // decrement it if it's not the end of the document + var endLineIndex = endLineInfo.index + if endLineInfo.range.lowerBound == range.upperBound + && endLineInfo.index != textView.layoutManager.lineCount - 1 { + endLineIndex -= 1 + } + + return startLineInfo.index...endLineIndex } private func adjustIndentation(lineIndexes: ClosedRange, inwards: Bool) { diff --git a/Tests/CodeEditSourceEditorTests/Controller/TextViewControllerTests.swift b/Tests/CodeEditSourceEditorTests/Controller/TextViewControllerTests.swift index 8088920ce..abac077dc 100644 --- a/Tests/CodeEditSourceEditorTests/Controller/TextViewControllerTests.swift +++ b/Tests/CodeEditSourceEditorTests/Controller/TextViewControllerTests.swift @@ -461,5 +461,31 @@ final class TextViewControllerTests: XCTestCase { XCTAssertEqual(controller.minimapView.frame.width, MinimapView.maxWidth) XCTAssertEqual(controller.textViewInsets.right, MinimapView.maxWidth) } + + // MARK: - Get Overlapping Lines + + func test_getOverlappingLines() { + controller.setText("A\nB\nC") + + // Select the entire first line, shouldn't include the second line + var lines = controller.getOverlappingLines(for: NSRange(location: 0, length: 2)) + XCTAssertEqual(0...0, lines) + + // Select the first char of the second line + lines = controller.getOverlappingLines(for: NSRange(location: 0, length: 3)) + XCTAssertEqual(0...1, lines) + + // Select the newline in the first line, and part of the second line + lines = controller.getOverlappingLines(for: NSRange(location: 1, length: 2)) + XCTAssertEqual(0...1, lines) + + // Select until the end of the document + lines = controller.getOverlappingLines(for: NSRange(location: 3, length: 2)) + XCTAssertEqual(1...2, lines) + + // Select just the last line of the document + lines = controller.getOverlappingLines(for: NSRange(location: 4, length: 1)) + XCTAssertEqual(2...2, lines) + } } // swiftlint:enable all From 01df82c694bdc00d4b16f29bf19ff6cacd0508bf Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Fri, 30 May 2025 11:01:47 -0500 Subject: [PATCH 2/2] lint:fix --- .../Controller/TextViewController+IndentLines.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+IndentLines.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+IndentLines.swift index 64207d6d8..2a4ae1254 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+IndentLines.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+IndentLines.swift @@ -128,7 +128,7 @@ extension TextViewController { } return false } - + /// Find the range of lines overlapping a text range. /// /// Use this method to determine what lines to apply a text transformation on using a text selection. For instance,