Skip to content

Commit 139a608

Browse files
committed
Port incremental parse ability to sourcekit-lsp
This feature will be used when we call `changeDocument` in SwiftLanguageServer
1 parent 869fd0a commit 139a608

File tree

2 files changed

+93
-7
lines changed

2 files changed

+93
-7
lines changed

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
119119

120120
var commandsByFile: [DocumentURI: SwiftCompileCommand] = [:]
121121

122+
var documentParseTransition: [DocumentURI: LookaheadRanges] = [:]
123+
124+
/// *For Testing*
125+
public var documentReusedNodeCollector: [DocumentURI: IncrementalParseReusedNodeCollector] = [:]
126+
122127
var keys: sourcekitd_keys { return sourcekitd.keys }
123128
var requests: sourcekitd_requests { return sourcekitd.requests }
124129
var values: sourcekitd_values { return sourcekitd.values }
@@ -198,13 +203,32 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
198203

199204
/// Returns the updated lexical tokens for the given `snapshot`.
200205
private func updateSyntaxTree(
201-
for snapshot: DocumentSnapshot
206+
for snapshot: DocumentSnapshot,
207+
with edits: ConcurrentEdits? = nil
202208
) -> DocumentTokens {
203209
logExecutionTime(level: .debug) {
204210
var docTokens = snapshot.tokens
211+
let documentURI = snapshot.document.uri
212+
213+
var reusedNodeCollector = documentReusedNodeCollector[documentURI]
214+
if reusedNodeCollector == nil {
215+
documentReusedNodeCollector[documentURI] = IncrementalParseReusedNodeCollector()
216+
reusedNodeCollector = documentReusedNodeCollector[documentURI]
217+
}
218+
219+
var parseTransition: IncrementalParseTransition? = nil
220+
if let previousTree = snapshot.tokens.syntaxTree,
221+
let lookaheadRanges = documentParseTransition[documentURI],
222+
let edits {
223+
parseTransition = IncrementalParseTransition(previousTree: previousTree, edits: edits, lookaheadRanges: lookaheadRanges, reusedNodeDelegate: reusedNodeCollector)
224+
}
225+
let (tree, nextLookaheadRanges) = Parser.parseIncrementally(
226+
source: snapshot.text, parseTransition: parseTransition)
205227

206-
docTokens.syntaxTree = Parser.parse(source: snapshot.text)
228+
docTokens.syntaxTree = tree
207229

230+
documentParseTransition[documentURI] = nextLookaheadRanges
231+
208232
return docTokens
209233
}
210234
}
@@ -520,34 +544,45 @@ extension SwiftLanguageServer {
520544
// Clear settings that should not be cached for closed documents.
521545
self.commandsByFile[uri] = nil
522546
self.currentDiagnostics[uri] = nil
547+
self.documentParseTransition[uri] = nil
548+
self.documentReusedNodeCollector[uri] = nil
523549

524550
_ = try? self.sourcekitd.sendSync(req)
525551
}
526552
}
527553

528554
public func changeDocument(_ note: DidChangeTextDocumentNotification) {
529555
let keys = self.keys
556+
var edits: [IncrementalEdit] = []
530557

531558
self.queue.async {
532559
var lastResponse: SKDResponseDictionary? = nil
533560

534-
let snapshot = self.documentManager.edit(note) { (before: DocumentSnapshot, edit: TextDocumentContentChangeEvent) in
561+
let snapshot = self.documentManager.edit(note) {
562+
(before: DocumentSnapshot, edit: TextDocumentContentChangeEvent) in
535563
let req = SKDRequestDictionary(sourcekitd: self.sourcekitd)
536564
req[keys.request] = self.requests.editor_replacetext
537565
req[keys.name] = note.textDocument.uri.pseudoPath
538566

539567
if let range = edit.range {
540-
guard let offset = before.utf8Offset(of: range.lowerBound), let end = before.utf8Offset(of: range.upperBound) else {
568+
guard let offset = before.utf8Offset(of: range.lowerBound),
569+
let end = before.utf8Offset(of: range.upperBound)
570+
else {
541571
fatalError("invalid edit \(range)")
542572
}
543573

574+
let length = end - offset
544575
req[keys.offset] = offset
545-
req[keys.length] = end - offset
576+
req[keys.length] = length
546577

578+
edits.append(IncrementalEdit(offset: offset, length: length, replacementLength: edit.text.utf8.count))
547579
} else {
548580
// Full text
581+
let length = before.text.utf8.count
549582
req[keys.offset] = 0
550-
req[keys.length] = before.text.utf8.count
583+
req[keys.length] = length
584+
585+
edits.append(IncrementalEdit(offset: 0, length: length, replacementLength: edit.text.utf8.count))
551586
}
552587

553588
req[keys.sourcetext] = edit.text
@@ -556,7 +591,7 @@ extension SwiftLanguageServer {
556591
self.adjustDiagnosticRanges(of: note.textDocument.uri, for: edit)
557592
} updateDocumentTokens: { (after: DocumentSnapshot) in
558593
if lastResponse != nil {
559-
return self.updateSyntaxTree(for: after)
594+
return self.updateSyntaxTree(for: after, with: ConcurrentEdits(fromSequential: edits))
560595
} else {
561596
return DocumentTokens()
562597
}

Tests/SourceKitLSPTests/LocalSwiftTests.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,4 +1476,55 @@ final class LocalSwiftTests: XCTestCase {
14761476
data = EditorPlaceholder(text)
14771477
XCTAssertNil(data)
14781478
}
1479+
1480+
func testIncrementalParse() throws {
1481+
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
1482+
let uri = DocumentURI(url)
1483+
1484+
sk.allowUnexpectedNotification = false
1485+
1486+
sk.send(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
1487+
uri: uri,
1488+
language: .swift,
1489+
version: 0,
1490+
text: """
1491+
func foo() {
1492+
}
1493+
class bar {
1494+
}
1495+
"""
1496+
)))
1497+
1498+
let didChangeTextDocumentExpectation = XCTestExpectation(description: "didChangeTextDocument")
1499+
sk.sendNoteSync(DidChangeTextDocumentNotification(textDocument: .init(uri, version: 1), contentChanges: [
1500+
.init(range: Range(Position(line: 2, utf16index: 7)), text: "a"),
1501+
]), { (note: LanguageServerProtocol.Notification<PublishDiagnosticsNotification>) -> Void in
1502+
log("Received diagnostics for text edit - syntactic")
1503+
didChangeTextDocumentExpectation.fulfill()
1504+
}, { (note: LanguageServerProtocol.Notification<PublishDiagnosticsNotification>) -> Void in
1505+
log("Received diagnostics for text edit - semantic")
1506+
})
1507+
1508+
let result = XCTWaiter.wait(for: [didChangeTextDocumentExpectation], timeout: defaultTimeout)
1509+
if result != .completed {
1510+
fatalError("error \(didChangeTextDocumentExpectation) waiting for changing text document notification")
1511+
}
1512+
1513+
let swiftLanguageServer = connection.server!._languageService(for: uri, .swift, in: connection.server!.workspaceForDocumentOnQueue(uri: uri)!) as! SwiftLanguageServer
1514+
1515+
guard let nodes = swiftLanguageServer.documentReusedNodeCollector[uri]?.nodes else {
1516+
XCTFail("Can not find reused nodes in \(uri)")
1517+
return
1518+
}
1519+
XCTAssertEqual(nodes.count, 1)
1520+
1521+
let firstNode = try XCTUnwrap(nodes.first)
1522+
XCTAssertEqual(firstNode.description,
1523+
"""
1524+
func foo() {
1525+
}
1526+
"""
1527+
)
1528+
XCTAssertEqual(firstNode.kind, .codeBlockItem)
1529+
}
14791530
}

0 commit comments

Comments
 (0)