Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions Sources/Runestone/TextView/Core/LineMovementController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ private extension LineMovementController {
return location
}
let lineLocalLocation = max(min(location - line.location, line.data.totalLength), 0)
// After an edit the line fragment tree is reset and only repopulated for lines laid out
// by the layout manager, so the query below may otherwise hit an empty tree.
// See https://github.com/simonbs/Runestone/issues/415
if lineController.numberOfLineFragments == 0 || !lineController.isFinishedTypesetting {
lineController.prepareToDisplayString(toLocation: lineLocalLocation, syntaxHighlightAsynchronously: true)
}
guard let lineFragmentNode = lineController.lineFragmentNode(containingCharacterAt: lineLocalLocation) else {
return location
}
Expand All @@ -78,7 +84,7 @@ private extension LineMovementController {
return locationForMovingDownwards(lineOffset: lineOffset, fromLocation: location, inLineFragmentAt: lineFragmentIndex, of: line)
} else {
// lineOffset is 0 so we shouldn't change the line
let lineController = lineControllerStorage.getOrCreateLineController(for: line)
let lineController = preparedLineController(for: line)
let destinationLineFragmentNode = lineController.lineFragmentNode(atIndex: lineFragmentIndex)
let lineLocation = line.location
let preferredLocation = lineLocation + destinationLineFragmentNode.location + location
Expand Down Expand Up @@ -132,7 +138,18 @@ private extension LineMovementController {
}

private func numberOfLineFragments(in line: DocumentLineNode) -> Int {
let lineController = lineControllerStorage.getOrCreateLineController(for: line)
let lineController = preparedLineController(for: line)
return lineController.numberOfLineFragments
}

// Ensures the entire line is typeset so its line fragments can be counted and queried.
// After an edit the line fragment tree is reset and only repopulated for lines laid out
// by the layout manager. See https://github.com/simonbs/Runestone/issues/415
private func preparedLineController(for line: DocumentLineNode) -> LineController {
let lineController = lineControllerStorage.getOrCreateLineController(for: line)
if lineController.numberOfLineFragments == 0 || !lineController.isFinishedTypesetting {
lineController.prepareToDisplayString(toLocation: line.data.totalLength, syntaxHighlightAsynchronously: true)
}
return lineController
}
}
19 changes: 17 additions & 2 deletions Sources/Runestone/TextView/Core/TextInputStringTokenizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ final class TextInputStringTokenizer: UITextInputStringTokenizer {

// MARK: - Lines
private extension TextInputStringTokenizer {
// Ensures the line is typeset far enough that its line fragment tree can answer queries
// at lineLocalLocation. After an edit the line fragment tree is reset and only repopulated
// for lines laid out by the layout manager, so UIKit's selection queries (e.g. during paste)
// may otherwise hit an empty tree. See https://github.com/simonbs/Runestone/issues/415
private func preparedLineController(for line: DocumentLineNode, toLocation lineLocalLocation: Int) -> LineController {
let lineController = lineControllerStorage.getOrCreateLineController(for: line)
if lineController.numberOfLineFragments == 0 || !lineController.isFinishedTypesetting {
lineController.prepareToDisplayString(toLocation: lineLocalLocation, syntaxHighlightAsynchronously: true)
}
return lineController
}

private func isPosition(_ position: UITextPosition, atLineBoundaryInDirection direction: UITextDirection) -> Bool {
guard let indexedPosition = position as? IndexedPosition else {
return false
Expand All @@ -59,10 +71,10 @@ private extension TextInputStringTokenizer {
}
let lineLocation = line.location
let lineLocalLocation = location - lineLocation
let lineController = lineControllerStorage.getOrCreateLineController(for: line)
guard lineLocalLocation >= 0 && lineLocalLocation <= line.data.totalLength else {
return false
}
let lineController = preparedLineController(for: line, toLocation: lineLocalLocation)
guard let lineFragmentNode = lineController.lineFragmentNode(containingCharacterAt: lineLocalLocation) else {
return false
}
Expand All @@ -86,9 +98,12 @@ private extension TextInputStringTokenizer {
guard let line = lineManager.line(containingCharacterAt: location) else {
return nil
}
let lineController = lineControllerStorage.getOrCreateLineController(for: line)
let lineLocation = line.location
let lineLocalLocation = location - lineLocation
guard lineLocalLocation >= 0 && lineLocalLocation <= line.data.totalLength else {
return nil
}
let lineController = preparedLineController(for: line, toLocation: lineLocalLocation)
guard let lineFragmentNode = lineController.lineFragmentNode(containingCharacterAt: lineLocalLocation) else {
return nil
}
Expand Down