import XCTest @testable import SaplingEditor final class DocumentLineIndexTests: XCTestCase { func testLFLineBoundaries() { assertLineIndex( source: "One\nTwo\nThree", expectedSources: ["One", "Two", "Three"], expectedRanges: [ NSRange(location: 0, length: 3), NSRange(location: 4, length: 3), NSRange(location: 8, length: 5) ], expectedEndings: [.lf, .lf, .none] ) } func testCRLFLineBoundaries() { assertLineIndex( source: "One\r\nTwo\r\nThree", expectedSources: ["One", "Two", "Three"], expectedRanges: [ NSRange(location: 0, length: 3), NSRange(location: 5, length: 3), NSRange(location: 10, length: 5) ], expectedEndings: [.crlf, .crlf, .none] ) } func testCRLineBoundaries() { assertLineIndex( source: "One\rTwo\rThree", expectedSources: ["One", "Two", "Three"], expectedRanges: [ NSRange(location: 0, length: 3), NSRange(location: 4, length: 3), NSRange(location: 8, length: 5) ], expectedEndings: [.cr, .cr, .none] ) } func testMixedLineBoundaries() { assertLineIndex( source: "One\nTwo\r\nThree\rFour", expectedSources: ["One", "Two", "Three", "Four"], expectedRanges: [ NSRange(location: 0, length: 3), NSRange(location: 4, length: 3), NSRange(location: 9, length: 5), NSRange(location: 15, length: 4) ], expectedEndings: [.lf, .crlf, .cr, .none] ) } func testTrailingBlankLineForEveryLineEnding() { XCTAssertEqual(DocumentLineIndex(source: "One\n").boundaries.map(\.contentRange.location), [0, 4]) XCTAssertEqual(DocumentLineIndex(source: "One\r\n").boundaries.map(\.contentRange.location), [0, 5]) XCTAssertEqual(DocumentLineIndex(source: "One\r").boundaries.map(\.contentRange.location), [0, 4]) } func testActiveLineDetectionAcrossCRLFBoundaries() { let source = "One\r\nTwo\r\nThree" XCTAssertEqual(EditorActiveLineTracker.lineIndex(containing: 0, in: source), 0) XCTAssertEqual(EditorActiveLineTracker.lineIndex(containing: 3, in: source), 0) XCTAssertEqual(EditorActiveLineTracker.lineIndex(containing: 4, in: source), 0) XCTAssertEqual(EditorActiveLineTracker.lineIndex(containing: 5, in: source), 1) XCTAssertEqual(EditorActiveLineTracker.lineIndex(containing: 8, in: source), 1) XCTAssertEqual(EditorActiveLineTracker.lineIndex(containing: 9, in: source), 1) XCTAssertEqual(EditorActiveLineTracker.lineIndex(containing: 10, in: source), 2) } func testEditorLinesPreserveCRLFSourceRangesAndModes() { let lines = EditorActiveLineTracker.lines(from: "One\r\nTwo\r\nThree", activeLineIndex: 1) XCTAssertEqual(lines.count, 3) XCTAssertEqual(lines.map(\.source), ["One", "Two", "Three"]) XCTAssertEqual(lines.map(\.range), [ NSRange(location: 0, length: 3), NSRange(location: 5, length: 3), NSRange(location: 10, length: 5) ]) XCTAssertEqual(lines.map(\.mode), [.rendered, .source, .rendered]) } func testOffsetAndLineMappingUsesCachedBoundaries() { let source = "One\nTwo\r\nThree\rFour" let index = DocumentLineIndex(source: source) XCTAssertEqual(index.lineCount, 4) XCTAssertEqual(index.lineStartOffset(forLine: 0), 0) XCTAssertEqual(index.lineStartOffset(forLine: 1), 4) XCTAssertEqual(index.lineStartOffset(forLine: 2), 9) XCTAssertEqual(index.lineStartOffset(forLine: 3), 15) XCTAssertEqual(index.lineIndex(containing: 0), 0) XCTAssertEqual(index.lineIndex(containing: 4), 1) XCTAssertEqual(index.lineIndex(containing: 14), 2) XCTAssertEqual(index.lineIndex(containing: 15), 3) } func testIncrementalInsertionWithoutNewlineMatchesFullRebuild() { assertIncrementalEdit( source: "One\nTwo\nThree", range: NSRange(location: 5, length: 0), replacement: " updated" ) } func testIncrementalInsertionWithLFMatchesFullRebuild() { assertIncrementalEdit( source: "One\nTwo\nThree", range: NSRange(location: 7, length: 0), replacement: "\nInserted" ) } func testIncrementalInsertionWithCRLFMatchesFullRebuild() { assertIncrementalEdit( source: "One\r\nTwo\r\nThree", range: NSRange(location: 8, length: 0), replacement: "\r\nInserted" ) } func testIncrementalDeletionAcrossLinesMatchesFullRebuild() { assertIncrementalEdit( source: "One\nTwo\nThree\nFour", range: NSRange(location: 2, length: 10), replacement: "" ) } func testIncrementalReplacementAcrossMixedLineEndingsMatchesFullRebuild() { assertIncrementalEdit( source: "One\nTwo\r\nThree\rFour", range: NSRange(location: 3, length: 11), replacement: "\r\nReplacement\n" ) } func testIncrementalEditAtCRLFBoundaryMatchesFullRebuild() { assertIncrementalEdit( source: "One\r\nTwo\r\nThree", range: NSRange(location: 3, length: 2), replacement: "\n" ) } func testIncrementalLargeDocumentEditMatchesFullRebuild() { let source = (0..<10_000) .map { "Line \($0)\r\n" } .joined() assertIncrementalEdit( source: source, range: NSRange(location: 42_000, length: 5), replacement: "changed\nwith\nnew lines" ) } func testBenchmarkDocumentSegmentsIntoPhysicalLines() throws { let url = URL(fileURLWithPath: "/Users/feror/Sapling/Docs/Benchmarks/5mb.md") let source = try String(contentsOf: url, encoding: .utf8) let lines = EditorActiveLineTracker.lines(from: source, activeLineIndex: 0) XCTAssertEqual(lines.count, 51_482) XCTAssertEqual(lines[0].source, "# ExampleFile.com - Example Files") XCTAssertEqual(lines[1].source, "") XCTAssertEqual(lines[2].source, "- ✨ExampleFile ✨") XCTAssertEqual(lines.filter { $0.mode == .source }.count, 1) XCTAssertEqual(lines.filter { $0.mode == .rendered }.count, 51_481) } private func assertLineIndex( source: String, expectedSources: [String], expectedRanges: [NSRange], expectedEndings: [LineEndingStrategy], file: StaticString = #filePath, line: UInt = #line ) { let index = DocumentLineIndex(source: source) let lines = index.editorLines(activeLineIndex: 0) XCTAssertEqual(lines.map(\.source), expectedSources, file: file, line: line) XCTAssertEqual(index.boundaries.map(\.contentRange), expectedRanges, file: file, line: line) XCTAssertEqual(index.boundaries.map(\.lineEnding), expectedEndings, file: file, line: line) } private func assertIncrementalEdit( source: String, range: NSRange, replacement: String, file: StaticString = #filePath, line: UInt = #line ) { var incremental = DocumentLineIndex(source: source) incremental.replaceCharacters(in: range, with: replacement) let rebuiltSource = (source as NSString).replacingCharacters(in: range, with: replacement) let rebuilt = DocumentLineIndex(source: rebuiltSource) XCTAssertEqual(incremental.source, rebuiltSource, file: file, line: line) XCTAssertEqual(incremental.boundaries, rebuilt.boundaries, file: file, line: line) XCTAssertEqual( incremental.editorLines(activeLineIndex: 0).map(\.source), rebuilt.editorLines(activeLineIndex: 0).map(\.source), file: file, line: line ) } }