diff --git a/Sources/CodeEditTextView/TextView/TextView+Move.swift b/Sources/CodeEditTextView/TextView/TextView+Move.swift index 6c2e3d50..d130f16e 100644 --- a/Sources/CodeEditTextView/TextView/TextView+Move.swift +++ b/Sources/CodeEditTextView/TextView/TextView+Move.swift @@ -8,7 +8,7 @@ import Foundation extension TextView { - fileprivate func updateAfterMove(direction: TextSelectionManager.Direction) { + fileprivate func updateAfterMove() { unmarkTextIfNeeded() scrollSelectionToVisible() } @@ -16,147 +16,147 @@ extension TextView { /// Moves the cursors up one character. override public func moveUp(_ sender: Any?) { selectionManager.moveSelections(direction: .up, destination: .character) - updateAfterMove(direction: .up) + updateAfterMove() } /// Moves the cursors up one character extending the current selection. override public func moveUpAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .up, destination: .character, modifySelection: true) - updateAfterMove(direction: .up) + updateAfterMove() } /// Moves the cursors down one character. override public func moveDown(_ sender: Any?) { selectionManager.moveSelections(direction: .down, destination: .character) - updateAfterMove(direction: .down) + updateAfterMove() } /// Moves the cursors down one character extending the current selection. override public func moveDownAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .down, destination: .character, modifySelection: true) - updateAfterMove(direction: .down) + updateAfterMove() } /// Moves the cursors left one character. override public func moveLeft(_ sender: Any?) { selectionManager.moveSelections(direction: .backward, destination: .character) - updateAfterMove(direction: .backward) + updateAfterMove() } /// Moves the cursors left one character extending the current selection. override public func moveLeftAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .backward, destination: .character, modifySelection: true) - updateAfterMove(direction: .backward) + updateAfterMove() } /// Moves the cursors right one character. override public func moveRight(_ sender: Any?) { selectionManager.moveSelections(direction: .forward, destination: .character) - updateAfterMove(direction: .forward) + updateAfterMove() } /// Moves the cursors right one character extending the current selection. override public func moveRightAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .forward, destination: .character, modifySelection: true) - updateAfterMove(direction: .forward) + updateAfterMove() } /// Moves the cursors left one word. override public func moveWordLeft(_ sender: Any?) { selectionManager.moveSelections(direction: .backward, destination: .word) - updateAfterMove(direction: .backward) + updateAfterMove() } /// Moves the cursors left one word extending the current selection. override public func moveWordLeftAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .backward, destination: .word, modifySelection: true) - updateAfterMove(direction: .backward) + updateAfterMove() } /// Moves the cursors right one word. override public func moveWordRight(_ sender: Any?) { selectionManager.moveSelections(direction: .forward, destination: .word) - updateAfterMove(direction: .forward) + updateAfterMove() } /// Moves the cursors right one word extending the current selection. override public func moveWordRightAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .forward, destination: .word, modifySelection: true) - updateAfterMove(direction: .forward) + updateAfterMove() } /// Moves the cursors left to the end of the line. override public func moveToLeftEndOfLine(_ sender: Any?) { selectionManager.moveSelections(direction: .backward, destination: .visualLine) - updateAfterMove(direction: .backward) + updateAfterMove() } /// Moves the cursors left to the end of the line extending the current selection. override public func moveToLeftEndOfLineAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .backward, destination: .visualLine, modifySelection: true) - updateAfterMove(direction: .backward) + updateAfterMove() } /// Moves the cursors right to the end of the line. override public func moveToRightEndOfLine(_ sender: Any?) { selectionManager.moveSelections(direction: .forward, destination: .visualLine) - updateAfterMove(direction: .forward) + updateAfterMove() } /// Moves the cursors right to the end of the line extending the current selection. override public func moveToRightEndOfLineAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .forward, destination: .visualLine, modifySelection: true) - updateAfterMove(direction: .forward) + updateAfterMove() } /// Moves the cursors to the beginning of the line, if pressed again selects the next line up. override public func moveToBeginningOfParagraph(_ sender: Any?) { selectionManager.moveSelections(direction: .up, destination: .line) - updateAfterMove(direction: .up) + updateAfterMove() } /// Moves the cursors to the beginning of the line, if pressed again selects the next line up extending the current /// selection. override public func moveToBeginningOfParagraphAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .up, destination: .line, modifySelection: true) - updateAfterMove(direction: .up) + updateAfterMove() } /// Moves the cursors to the end of the line, if pressed again selects the next line up. override public func moveToEndOfParagraph(_ sender: Any?) { selectionManager.moveSelections(direction: .down, destination: .line) - updateAfterMove(direction: .down) + updateAfterMove() } /// Moves the cursors to the end of the line, if pressed again selects the next line up extending the current /// selection. override public func moveToEndOfParagraphAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .down, destination: .line, modifySelection: true) - updateAfterMove(direction: .down) + updateAfterMove() } /// Moves the cursors to the beginning of the document. override public func moveToBeginningOfDocument(_ sender: Any?) { selectionManager.moveSelections(direction: .up, destination: .document) - updateAfterMove(direction: .up) + updateAfterMove() } /// Moves the cursors to the beginning of the document extending the current selection. override public func moveToBeginningOfDocumentAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .up, destination: .document, modifySelection: true) - updateAfterMove(direction: .up) + updateAfterMove() } /// Moves the cursors to the end of the document. override public func moveToEndOfDocument(_ sender: Any?) { selectionManager.moveSelections(direction: .down, destination: .document) - updateAfterMove(direction: .down) + updateAfterMove() } /// Moves the cursors to the end of the document extending the current selection. override public func moveToEndOfDocumentAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .down, destination: .document, modifySelection: true) - updateAfterMove(direction: .down) + updateAfterMove() } override public func pageUp(_ sender: Any?) { @@ -165,7 +165,7 @@ extension TextView { override public func pageUpAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .up, destination: .page, modifySelection: true) - updateAfterMove(direction: .up) + updateAfterMove() } override public func pageDown(_ sender: Any?) { @@ -174,6 +174,6 @@ extension TextView { override public func pageDownAndModifySelection(_ sender: Any?) { selectionManager.moveSelections(direction: .down, destination: .page, modifySelection: true) - updateAfterMove(direction: .down) + updateAfterMove() } } diff --git a/Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift b/Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift index ffcc047c..111b1285 100644 --- a/Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift +++ b/Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift @@ -8,22 +8,24 @@ import Foundation extension TextView { + fileprivate typealias Direction = TextSelectionManager.Direction + fileprivate typealias TextSelection = TextSelectionManager.TextSelection + /// Scrolls the upmost selection to the visible rect if `scrollView` is not `nil`. - /// - Parameter updateDirection: (optional) the direction of a change in selection. Used to try and keep - /// contextual portions of the selection in the viewport. - public func scrollSelectionToVisible(updateDirection: TextSelectionManager.Direction? = nil) { - guard let scrollView else { + public func scrollSelectionToVisible() { + guard let scrollView, let selection = getSelection() else { return } + let offsetToScrollTo = offsetNotPivot(selection) + // There's a bit of a chicken-and-the-egg issue going on here. We need to know the rect to scroll to, but we // can't know the exact rect to make visible without laying out the text. Then, once text is laid out the // selection rect may be different again. To solve this, we loop until the frame doesn't change after a layout // pass and scroll to that rect. var lastFrame: CGRect = .zero - while let boundingRect = getSelectionRect(updateDirection), - lastFrame != boundingRect { + while let boundingRect = layoutManager.rectForOffset(offsetToScrollTo), lastFrame != boundingRect { lastFrame = boundingRect layoutManager.layoutLines() selectionManager.updateSelectionViews() @@ -34,42 +36,26 @@ extension TextView { } } - /// Get the rect that should be scrolled to visible for the current text selection. - /// - Parameter updateDirection: The direction of the update. - /// - Returns: The rect of the selection. - private func getSelectionRect(_ updateDirection: TextSelectionManager.Direction?) -> CGRect? { - switch updateDirection { - case .forward, .backward, nil: - return selectionManager - .textSelections - .sorted(by: { $0.boundingRect.origin.y < $1.boundingRect.origin.y }) - .first? - .boundingRect - case .up: - guard let selection = selectionManager - .textSelections - .sorted(by: { $0.range.location < $1.range.location }) // Get the highest one. - .first, - let minRect = layoutManager.rectForOffset(selection.range.location) else { - return nil - } - return CGRect( - origin: minRect.origin, - size: CGSize(width: selection.boundingRect.width, height: layoutManager.estimateLineHeight()) - ) - case .down: - guard let selection = selectionManager - .textSelections - .sorted(by: { $0.range.max > $1.range.max }) // Get the lowest one. - .first, - let maxRect = layoutManager.rectForOffset(selection.range.max) else { - return nil - } - let lineHeight = layoutManager.estimateLineHeight() - return CGRect( - origin: CGPoint(x: selection.boundingRect.origin.x, y: maxRect.maxY - lineHeight), - size: CGSize(width: selection.boundingRect.width, height: lineHeight) - ) + /// Get the selection that should be scrolled to visible for the current text selection. + /// - Returns: The the selection to scroll to. + private func getSelection() -> TextSelection? { + selectionManager + .textSelections + .sorted(by: { $0.range.max > $1.range.max }) // Get the lowest one. + .first + } + + /// Returns the offset that isn't the pivot of the selection. + /// - Parameter selection: The selection to use. + /// - Returns: The offset suitable for scrolling to. + private func offsetNotPivot(_ selection: TextSelection) -> Int { + guard let pivot = selection.pivot else { + return selection.range.location + } + if selection.range.location == pivot { + return selection.range.max + } else { + return selection.range.location } } }