Skip to content

Pull SwiftSyntax into SourceKit-LSP #651

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 20, 2022
Merged
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ find_package(LLBuild QUIET)
find_package(ArgumentParser CONFIG REQUIRED)
find_package(SwiftCollections QUIET)
find_package(SwiftSystem CONFIG REQUIRED)
find_package(SwiftSyntax CONFIG REQUIRED)

include(SwiftSupport)

Expand Down
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ let package = Package(
"SourceKitD",
"SKSwiftPMWorkspace",
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
],
exclude: ["CMakeLists.txt"]),

Expand Down Expand Up @@ -241,12 +243,14 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
.package(name: "SwiftPM", url: "https://github.com/apple/swift-package-manager.git", .branch("main")),
.package(url: "https://github.com/apple/swift-tools-support-core.git", .branch("main")),
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.0.1")),
.package(url: "https://github.com/apple/swift-syntax.git", .branch("main")),
]
} else {
package.dependencies += [
.package(name: "IndexStoreDB", path: "../indexstore-db"),
.package(name: "SwiftPM", path: "../swiftpm"),
.package(path: "../swift-tools-support-core"),
.package(path: "../swift-argument-parser")
.package(path: "../swift-argument-parser"),
.package(path: "../swift-syntax")
]
}
5 changes: 5 additions & 0 deletions Sources/SourceKitLSP/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ target_link_libraries(SourceKitLSP PUBLIC
SKCore
SKSwiftPMWorkspace
SourceKitD
SwiftSyntax::SwiftBasicFormat
SwiftSyntax::SwiftParser
SwiftSyntax::SwiftDiagnostics
SwiftSyntax::SwiftSyntax
TSCUtility)
target_link_libraries(SourceKitLSP PRIVATE
$<$<NOT:$<PLATFORM_ID:Darwin>>:FoundationXML>)

19 changes: 8 additions & 11 deletions Sources/SourceKitLSP/DocumentManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,14 @@ public final class DocumentManager {
// Remove all tokens in the updated range and shift later ones.
let rangeAdjuster = RangeAdjuster(edit: edit)!

document.latestTokens.withMutableTokensOfEachKind { tokens in
tokens = Array(tokens.lazy
.compactMap {
var token = $0
if let adjustedRange = rangeAdjuster.adjust(token.range) {
token.range = adjustedRange
return token
} else {
return nil
}
})
document.latestTokens.semantic = document.latestTokens.semantic.compactMap {
var token = $0
if let adjustedRange = rangeAdjuster.adjust(token.range) {
token.range = adjustedRange
return token
} else {
return nil
}
}
} else {
// Full text replacement.
Expand Down
99 changes: 84 additions & 15 deletions Sources/SourceKitLSP/DocumentTokens.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,100 @@
//===----------------------------------------------------------------------===//

import LanguageServerProtocol
import SwiftSyntax

/// Syntax highlighting tokens for a particular document.
public struct DocumentTokens {
/// Lexical tokens, e.g. keywords, raw identifiers, ...
public var lexical: [SyntaxHighlightingToken] = []
/// The syntax tree representing the entire document.
public var syntaxTree: SourceFileSyntax?
/// Semantic tokens, e.g. variable references, type references, ...
public var semantic: [SyntaxHighlightingToken] = []
}

private var merged: [SyntaxHighlightingToken] {
lexical.mergingTokens(with: semantic)
extension DocumentSnapshot {
/// Computes an array of syntax highlighting tokens from the syntax tree that
/// have been merged with any semantic tokens from SourceKit. If the provided
/// range is non-empty, this function restricts its output to only those
/// tokens whose ranges overlap it. If no range is provided, tokens for the
/// entire document are returned.
///
/// - Parameter range: The range of tokens to restrict this function to, if any.
/// - Returns: An array of syntax highlighting tokens.
public func mergedAndSortedTokens(in range: Range<Position>? = nil) -> [SyntaxHighlightingToken] {
guard let tree = self.tokens.syntaxTree else {
return self.tokens.semantic
}
let range = range.flatMap({ $0.byteSourceRange(in: self) })
?? ByteSourceRange(offset: 0, length: tree.byteSize)
return tree
.classifications(in: range)
.flatMap({ $0.highlightingTokens(in: self) })
.mergingTokens(with: self.tokens.semantic)
.sorted { $0.start < $1.start }
}
public var mergedAndSorted: [SyntaxHighlightingToken] {
merged.sorted { $0.start < $1.start }
}

extension Range where Bound == Position {
fileprivate func byteSourceRange(in snapshot: DocumentSnapshot) -> ByteSourceRange? {
return snapshot.utf8OffsetRange(of: self).map({ ByteSourceRange(offset: $0.startIndex, length: $0.count) })
}
}

/// Modifies the syntax highlighting tokens of each kind
/// (lexical and semantic) according to `action`.
public mutating func withMutableTokensOfEachKind(_ action: (inout [SyntaxHighlightingToken]) -> Void) {
action(&lexical)
action(&semantic)
extension SyntaxClassifiedRange {
fileprivate func highlightingTokens(in snapshot: DocumentSnapshot) -> [SyntaxHighlightingToken] {
guard let (kind, modifiers) = self.kind.highlightingKindAndModifiers else {
return []
}

guard
let start: Position = snapshot.positionOf(utf8Offset: self.offset),
let end: Position = snapshot.positionOf(utf8Offset: self.endOffset)
else {
return []
}

let multiLineRange = start..<end
let ranges = multiLineRange.splitToSingleLineRanges(in: snapshot)

return ranges.map {
SyntaxHighlightingToken(
range: $0,
kind: kind,
modifiers: modifiers
)
}
}
}

// Replace all lexical tokens in `range`.
public mutating func replaceLexical(in range: Range<Position>, with newTokens: [SyntaxHighlightingToken]) {
lexical.removeAll { $0.range.overlaps(range) }
lexical += newTokens
extension SyntaxClassification {
fileprivate var highlightingKindAndModifiers: (SyntaxHighlightingToken.Kind, SyntaxHighlightingToken.Modifiers)? {
switch self {
case .none:
return nil
case .editorPlaceholder:
return nil
case .stringInterpolationAnchor:
return nil
case .keyword:
return (.keyword, [])
case .identifier, .typeIdentifier, .dollarIdentifier:
return (.identifier, [])
case .operatorIdentifier:
return (.operator, [])
case .integerLiteral, .floatingLiteral:
return (.number, [])
case .stringLiteral:
return (.string, [])
case .poundDirectiveKeyword:
return (.macro, [])
case .buildConfigId, .objectLiteral:
return (.macro, [])
case .attribute:
return (.modifier, [])
case .lineComment, .blockComment:
return (.comment, [])
case .docLineComment, .docBlockComment:
return (.comment, .documentation)
}
}
}
41 changes: 12 additions & 29 deletions Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import LSPLogging
import SKCore
import SKSupport
import SourceKitD
import SwiftSyntax
import SwiftParser

#if os(Windows)
import WinSDK
Expand Down Expand Up @@ -167,14 +169,13 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {

/// Updates the lexical tokens for the given `snapshot`.
/// Must be called on `self.queue`.
private func updateLexicalTokens(
response: SKDResponseDictionary,
private func updateSyntacticTokens(
for snapshot: DocumentSnapshot
) {
dispatchPrecondition(condition: .onQueue(queue))

let uri = snapshot.document.uri
let docTokens = updatedLexicalTokens(response: response, for: snapshot)
let docTokens = updateSyntaxTree(for: snapshot)

do {
try documentManager.updateTokens(uri, tokens: docTokens)
Expand All @@ -184,31 +185,13 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
}

/// Returns the updated lexical tokens for the given `snapshot`.
private func updatedLexicalTokens(
response: SKDResponseDictionary,
private func updateSyntaxTree(
for snapshot: DocumentSnapshot
) -> DocumentTokens {
logExecutionTime(level: .debug) {
var docTokens = snapshot.tokens

guard let offset: Int = response[keys.offset],
let length: Int = response[keys.length],
let start: Position = snapshot.positionOf(utf8Offset: offset),
let end: Position = snapshot.positionOf(utf8Offset: offset + length) else {
// This e.g. happens in the case of empty edits
log("did not update lexical/syntactic tokens, no range found", level: .debug)
return docTokens
}

let range = start..<end

if let syntaxMap: SKDResponseArray = response[keys.syntaxmap] {
let tokenParser = SyntaxHighlightingTokenParser(sourcekitd: sourcekitd)
var tokens: [SyntaxHighlightingToken] = []
tokenParser.parseTokens(syntaxMap, in: snapshot, into: &tokens)

docTokens.replaceLexical(in: range, with: tokens)
}
docTokens.syntaxTree = Parser.parse(source: snapshot.text)

return docTokens
}
Expand Down Expand Up @@ -438,7 +421,7 @@ extension SwiftLanguageServer {
}
self.publishDiagnostics(
response: dict, for: snapshot, compileCommand: compileCmd)
self.updateLexicalTokens(response: dict, for: snapshot)
self.updateSyntacticTokens(for: snapshot)
}

public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) {
Expand Down Expand Up @@ -503,7 +486,7 @@ extension SwiftLanguageServer {
return
}
self.publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand)
self.updateLexicalTokens(response: dict, for: snapshot)
self.updateSyntacticTokens(for: snapshot)
}
}

Expand Down Expand Up @@ -557,8 +540,8 @@ extension SwiftLanguageServer {

self.adjustDiagnosticRanges(of: note.textDocument.uri, for: edit)
} updateDocumentTokens: { (after: DocumentSnapshot) in
if let dict = lastResponse {
return self.updatedLexicalTokens(response: dict, for: after)
if lastResponse != nil {
return self.updateSyntaxTree(for: after)
} else {
return DocumentTokens()
}
Expand Down Expand Up @@ -876,7 +859,7 @@ extension SwiftLanguageServer {
return
}

let tokens = snapshot.tokens.mergedAndSorted
let tokens = snapshot.mergedAndSortedTokens()
let encodedTokens = tokens.lspEncoded

req.reply(DocumentSemanticTokensResponse(data: encodedTokens))
Expand All @@ -899,7 +882,7 @@ extension SwiftLanguageServer {
return
}

let tokens = snapshot.tokens.mergedAndSorted.filter { $0.range.overlaps(range) }
let tokens = snapshot.mergedAndSortedTokens(in: range)
let encodedTokens = tokens.lspEncoded

req.reply(DocumentSemanticTokensResponse(data: encodedTokens))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ struct SyntaxHighlightingTokenParser {

extension Range where Bound == Position {
/// Splits a potentially multi-line range to multiple single-line ranges.
fileprivate func splitToSingleLineRanges(in snapshot: DocumentSnapshot) -> [Self] {
func splitToSingleLineRanges(in snapshot: DocumentSnapshot) -> [Self] {
if isEmpty {
return []
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/SourceKitLSPTests/SemanticTokensTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ final class SemanticTokensTests: XCTestCase {
"""
let tokens = openAndPerformSemanticTokensRequest(text: text)
XCTAssertEqual(tokens, [
Token(line: 0, utf16index: 0, length: 5, kind: .modifier),
Token(line: 0, utf16index: 0, length: 5, kind: .keyword),
Token(line: 0, utf16index: 6, length: 8, kind: .keyword),
Token(line: 0, utf16index: 15, length: 2, kind: .operator),
Token(line: 0, utf16index: 19, length: 20, kind: .identifier),
Expand Down