125 lines
3.8 KiB
Swift
125 lines
3.8 KiB
Swift
import Foundation
|
|
|
|
public struct EditableRegion: Hashable, Sendable {
|
|
public var lineIndexes: [Int]
|
|
|
|
public init(lineIndexes: some Sequence<Int>) {
|
|
self.lineIndexes = Array(Set(lineIndexes)).sorted()
|
|
}
|
|
|
|
public var primaryLineIndex: Int {
|
|
lineIndexes.first ?? -1
|
|
}
|
|
|
|
public var isEmpty: Bool {
|
|
lineIndexes.isEmpty
|
|
}
|
|
|
|
public func contains(_ lineIndex: Int) -> Bool {
|
|
lineIndexes.binarySearch(lineIndex)
|
|
}
|
|
|
|
public static func none() -> EditableRegion {
|
|
EditableRegion(lineIndexes: [])
|
|
}
|
|
|
|
public static func selection(_ selection: NSRange, in lineIndex: DocumentLineIndex) -> EditableRegion {
|
|
guard lineIndex.lineCount > 0 else {
|
|
return .none()
|
|
}
|
|
|
|
let sourceLength = lineIndex.source.utf16.count
|
|
let startLocation = max(0, min(selection.location, sourceLength))
|
|
let endLocation: Int
|
|
if selection.length == 0 {
|
|
endLocation = startLocation
|
|
} else {
|
|
endLocation = max(startLocation, min(selection.upperBound - 1, sourceLength))
|
|
}
|
|
|
|
let startLine = lineIndex.lineIndex(containing: startLocation)
|
|
let endLine = lineIndex.lineIndex(containing: endLocation)
|
|
let selectedRange = min(startLine, endLine)...max(startLine, endLine)
|
|
var editableLines = Set(selectedRange)
|
|
guard lineIndex.source.contains("```") || lineIndex.source.contains("~~~") else {
|
|
return EditableRegion(lineIndexes: editableLines)
|
|
}
|
|
|
|
for selectedLine in selectedRange {
|
|
if let codeBlockRange = lineIndex.fencedCodeBlockLineRange(containing: selectedLine) {
|
|
editableLines.formUnion(codeBlockRange)
|
|
}
|
|
}
|
|
|
|
return EditableRegion(lineIndexes: editableLines)
|
|
}
|
|
}
|
|
|
|
private extension DocumentLineIndex {
|
|
func fencedCodeBlockLineRange(containing lineIndex: Int) -> ClosedRange<Int>? {
|
|
guard (0..<lineCount).contains(lineIndex) else { return nil }
|
|
|
|
var openLineIndex: Int?
|
|
var openFence: String?
|
|
for currentLineIndex in 0..<lineCount {
|
|
guard let line = editorLine(at: currentLineIndex, activeLineIndex: -1),
|
|
let fence = Self.fenceMarker(in: line.source)
|
|
else { continue }
|
|
|
|
if let start = openLineIndex {
|
|
if fence == openFence {
|
|
let blockRange = start...currentLineIndex
|
|
return blockRange.contains(lineIndex) ? blockRange : nil
|
|
}
|
|
} else {
|
|
openLineIndex = currentLineIndex
|
|
openFence = fence
|
|
}
|
|
}
|
|
|
|
if let start = openLineIndex {
|
|
let blockRange = start...max(start, lineCount - 1)
|
|
return blockRange.contains(lineIndex) ? blockRange : nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
static func fenceMarker(in source: String) -> String? {
|
|
let indentation = source.prefix { $0 == " " || $0 == "\t" }
|
|
guard indentation.count <= 3 else { return nil }
|
|
|
|
let content = source.dropFirst(indentation.count)
|
|
if content.hasPrefix("```") {
|
|
return "```"
|
|
}
|
|
if content.hasPrefix("~~~") {
|
|
return "~~~"
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private extension Array where Element == Int {
|
|
func binarySearch(_ value: Int) -> Bool {
|
|
var lowerBound = 0
|
|
var upperBound = count - 1
|
|
while lowerBound <= upperBound {
|
|
let midpoint = (lowerBound + upperBound) / 2
|
|
if self[midpoint] == value {
|
|
return true
|
|
}
|
|
if self[midpoint] < value {
|
|
lowerBound = midpoint + 1
|
|
} else {
|
|
upperBound = midpoint - 1
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
private extension NSRange {
|
|
var upperBound: Int {
|
|
location + length
|
|
}
|
|
}
|