Skip to content

Commit ab289aa

Browse files
authored
Merge pull request #761 from StevenWong12/enable_incremental_parse
2 parents 644214a + f1d182e commit ab289aa

File tree

3 files changed

+84
-7
lines changed

3 files changed

+84
-7
lines changed

Sources/SourceKitLSP/DocumentTokens.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
import LanguageServerProtocol
1414
import SwiftSyntax
1515
import SwiftIDEUtils
16+
import SwiftParser
1617

1718
/// Syntax highlighting tokens for a particular document.
1819
public struct DocumentTokens {
1920
/// The syntax tree representing the entire document.
2021
public var syntaxTree: SourceFileSyntax?
22+
/// This information is used to determine whether a syntax node can be re-used in incremental parsing.
23+
///
24+
/// The property is not nil only after the document is parsed.
25+
public var lookaheadRanges: LookaheadRanges?
2126
/// Semantic tokens, e.g. variable references, type references, ...
2227
public var semantic: [SyntaxHighlightingToken] = []
2328
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
118118
var currentCompletionSession: CodeCompletionSession? = nil
119119

120120
var commandsByFile: [DocumentURI: SwiftCompileCommand] = [:]
121+
122+
/// *For Testing*
123+
public var reusedNodeCallback: ReusedNodeCallback?
121124

122125
var keys: sourcekitd_keys { return sourcekitd.keys }
123126
var requests: sourcekitd_requests { return sourcekitd.requests }
@@ -197,13 +200,27 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
197200
}
198201

199202
/// Returns the updated lexical tokens for the given `snapshot`.
203+
///
204+
/// - Parameters:
205+
/// - edits: If we are in the context of editing the contents of a file, i.e. calling ``SwiftLanguageServer/changeDocument(_:)``, we should pass `edits` to enable incremental parse. Otherwise, `edits` should be `nil`.
200206
private func updateSyntaxTree(
201-
for snapshot: DocumentSnapshot
207+
for snapshot: DocumentSnapshot,
208+
with edits: ConcurrentEdits? = nil
202209
) -> DocumentTokens {
203210
logExecutionTime(level: .debug) {
204211
var docTokens = snapshot.tokens
212+
213+
var parseTransition: IncrementalParseTransition? = nil
214+
if let previousTree = snapshot.tokens.syntaxTree,
215+
let lookaheadRanges = snapshot.tokens.lookaheadRanges,
216+
let edits {
217+
parseTransition = IncrementalParseTransition(previousTree: previousTree, edits: edits, lookaheadRanges: lookaheadRanges, reusedNodeCallback: reusedNodeCallback)
218+
}
219+
let (tree, nextLookaheadRanges) = Parser.parseIncrementally(
220+
source: snapshot.text, parseTransition: parseTransition)
205221

206-
docTokens.syntaxTree = Parser.parse(source: snapshot.text)
222+
docTokens.syntaxTree = tree
223+
docTokens.lookaheadRanges = nextLookaheadRanges
207224

208225
return docTokens
209226
}
@@ -527,27 +544,36 @@ extension SwiftLanguageServer {
527544

528545
public func changeDocument(_ note: DidChangeTextDocumentNotification) {
529546
let keys = self.keys
547+
var edits: [IncrementalEdit] = []
530548

531549
self.queue.async {
532550
var lastResponse: SKDResponseDictionary? = nil
533551

534-
let snapshot = self.documentManager.edit(note) { (before: DocumentSnapshot, edit: TextDocumentContentChangeEvent) in
552+
let snapshot = self.documentManager.edit(note) {
553+
(before: DocumentSnapshot, edit: TextDocumentContentChangeEvent) in
535554
let req = SKDRequestDictionary(sourcekitd: self.sourcekitd)
536555
req[keys.request] = self.requests.editor_replacetext
537556
req[keys.name] = note.textDocument.uri.pseudoPath
538557

539558
if let range = edit.range {
540-
guard let offset = before.utf8Offset(of: range.lowerBound), let end = before.utf8Offset(of: range.upperBound) else {
559+
guard let offset = before.utf8Offset(of: range.lowerBound),
560+
let end = before.utf8Offset(of: range.upperBound)
561+
else {
541562
fatalError("invalid edit \(range)")
542563
}
543564

565+
let length = end - offset
544566
req[keys.offset] = offset
545-
req[keys.length] = end - offset
567+
req[keys.length] = length
546568

569+
edits.append(IncrementalEdit(offset: offset, length: length, replacementLength: edit.text.utf8.count))
547570
} else {
548571
// Full text
572+
let length = before.text.utf8.count
549573
req[keys.offset] = 0
550-
req[keys.length] = before.text.utf8.count
574+
req[keys.length] = length
575+
576+
edits.append(IncrementalEdit(offset: 0, length: length, replacementLength: edit.text.utf8.count))
551577
}
552578

553579
req[keys.sourcetext] = edit.text
@@ -556,7 +582,7 @@ extension SwiftLanguageServer {
556582
self.adjustDiagnosticRanges(of: note.textDocument.uri, for: edit)
557583
} updateDocumentTokens: { (after: DocumentSnapshot) in
558584
if lastResponse != nil {
559-
return self.updateSyntaxTree(for: after)
585+
return self.updateSyntaxTree(for: after, with: ConcurrentEdits(fromSequential: edits))
560586
} else {
561587
return DocumentTokens()
562588
}

Tests/SourceKitLSPTests/LocalSwiftTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import LSPTestSupport
1616
import SKTestSupport
1717
import SourceKitLSP
1818
import XCTest
19+
import SwiftSyntax
1920

2021
// Workaround ambiguity with Foundation.
2122
typealias Notification = LanguageServerProtocol.Notification
@@ -1476,4 +1477,49 @@ final class LocalSwiftTests: XCTestCase {
14761477
data = EditorPlaceholder(text)
14771478
XCTAssertNil(data)
14781479
}
1480+
1481+
func testIncrementalParse() throws {
1482+
let url = URL(fileURLWithPath: "/\(UUID())/a.swift")
1483+
let uri = DocumentURI(url)
1484+
1485+
var reusedNodes: [Syntax] = []
1486+
let swiftLanguageServer = connection.server!._languageService(for: uri, .swift, in: connection.server!.workspaceForDocumentOnQueue(uri: uri)!) as! SwiftLanguageServer
1487+
swiftLanguageServer.reusedNodeCallback = { reusedNodes.append($0) }
1488+
sk.allowUnexpectedNotification = false
1489+
1490+
sk.send(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
1491+
uri: uri,
1492+
language: .swift,
1493+
version: 0,
1494+
text: """
1495+
func foo() {
1496+
}
1497+
class bar {
1498+
}
1499+
"""
1500+
)))
1501+
1502+
let didChangeTextDocumentExpectation = self.expectation(description: "didChangeTextDocument")
1503+
sk.sendNoteSync(DidChangeTextDocumentNotification(textDocument: .init(uri, version: 1), contentChanges: [
1504+
.init(range: Range(Position(line: 2, utf16index: 7)), text: "a"),
1505+
]), { (note: LanguageServerProtocol.Notification<PublishDiagnosticsNotification>) -> Void in
1506+
log("Received diagnostics for text edit - syntactic")
1507+
didChangeTextDocumentExpectation.fulfill()
1508+
}, { (note: LanguageServerProtocol.Notification<PublishDiagnosticsNotification>) -> Void in
1509+
log("Received diagnostics for text edit - semantic")
1510+
})
1511+
1512+
self.wait(for: [didChangeTextDocumentExpectation], timeout: defaultTimeout)
1513+
1514+
XCTAssertEqual(reusedNodes.count, 1)
1515+
1516+
let firstNode = try XCTUnwrap(reusedNodes.first)
1517+
XCTAssertEqual(firstNode.description,
1518+
"""
1519+
func foo() {
1520+
}
1521+
"""
1522+
)
1523+
XCTAssertEqual(firstNode.kind, .codeBlockItem)
1524+
}
14791525
}

0 commit comments

Comments
 (0)