mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
509 lines
20 KiB
Swift
509 lines
20 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2024 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import CompletionScoring
|
|
import Csourcekitd
|
|
import Foundation
|
|
import SKLogging
|
|
import SourceKitD
|
|
import SwiftSourceKitPluginCommon
|
|
|
|
/// Parse a `[String: Popularity]` dictionary from an array of XPC dictionaries that looks as follows:
|
|
/// ```
|
|
/// [
|
|
/// {
|
|
/// "key.popularity.key": <some-module-name>,
|
|
/// "key.popularity.value.int.billion": <popularity-multiplied-by-one-billion>
|
|
/// },
|
|
/// ...
|
|
/// ]
|
|
/// ```
|
|
/// If a key occurs twice, we use the later value.
|
|
/// Returns `nil` if parsing failed because one of he entries didn't contain a key or value.
|
|
private func parsePopularityDict(_ data: SKDRequestArrayReader) -> [String: Popularity]? {
|
|
var result: [String: Popularity] = [:]
|
|
let iteratedAllEntries = data.forEach { (_, entry) -> Bool in
|
|
// We can't deserialize double values in SourceKit requests at the moment.
|
|
// We transfer the double value as an integer with 9 significant digits by multiplying it by 1 billion first.
|
|
guard let key: String = entry[entry.sourcekitd.keys.popularityKey],
|
|
let value: Int = entry[entry.sourcekitd.keys.popularityValueIntBillion]
|
|
else {
|
|
return false
|
|
}
|
|
result[key] = Popularity(scoreComponent: Double(value) / 1_000_000_000)
|
|
return true
|
|
}
|
|
if !iteratedAllEntries {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
extension PopularityTable {
|
|
/// Create a PopularityTable from a serialized XPC form that looks as follows:
|
|
/// ```
|
|
/// {
|
|
/// "key.symbol_popularity": [ <see parsePopularityDict> ],
|
|
/// "key.module_popularity": [ <see parsePopularityDict> ],
|
|
/// }
|
|
/// ```
|
|
/// Returns `nil` if the dictionary didn't match the expected format.
|
|
init?(_ dict: SKDRequestDictionaryReader) {
|
|
let keys = dict.sourcekitd.keys
|
|
guard let symbolPopularityData: SKDRequestArrayReader = dict[keys.symbolPopularity],
|
|
let symbolPopularity = parsePopularityDict(symbolPopularityData),
|
|
let modulePopularityData: SKDRequestArrayReader = dict[keys.modulePopularity],
|
|
let modulePopularity = parsePopularityDict(modulePopularityData)
|
|
else {
|
|
return nil
|
|
}
|
|
self.init(symbolPopularity: symbolPopularity, modulePopularity: modulePopularity)
|
|
}
|
|
}
|
|
|
|
actor CompletionProvider {
|
|
enum InvalidRequest: SourceKitPluginError {
|
|
case missingKey(String)
|
|
|
|
func response(sourcekitd: SourceKitD) -> SKDResponse {
|
|
switch self {
|
|
case .missingKey(let key):
|
|
return SKDResponse(error: .invalid, description: "missing required key '\(key)'", sourcekitd: sourcekitd)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let logger = Logger(subsystem: "org.swift.sourcekit.service-plugin", category: "CompletionProvider")
|
|
|
|
private let connection: Connection
|
|
|
|
/// See `Connection.cancellationFunc`
|
|
private nonisolated let cancel: @Sendable (RequestHandle) -> Void
|
|
|
|
/// The XPC custom buffer kind for `CompletionResultsArray`
|
|
private let completionResultsBufferKind: UInt64
|
|
|
|
/// The code completion session that's currently open.
|
|
private var currentSession: CompletionSession? = nil
|
|
|
|
init(
|
|
completionResultsBufferKind: UInt64,
|
|
opaqueIDEInspectionInstance: OpaqueIDEInspectionInstance? = nil,
|
|
sourcekitd: SourceKitD
|
|
) {
|
|
self.connection = Connection(
|
|
opaqueIDEInspectionInstance: opaqueIDEInspectionInstance?.value,
|
|
sourcekitd: sourcekitd
|
|
)
|
|
self.cancel = connection.cancellationFunc
|
|
self.completionResultsBufferKind = completionResultsBufferKind
|
|
}
|
|
|
|
nonisolated func cancel(handle: RequestHandle) {
|
|
self.cancel(handle)
|
|
}
|
|
|
|
func handleDocumentOpen(_ request: SKDRequestDictionaryReader) {
|
|
let keys = request.sourcekitd.keys
|
|
guard let path: String = request[keys.name] else {
|
|
self.logger.error("error: dropping request editor.open: missing 'key.name'")
|
|
return
|
|
}
|
|
let content: String
|
|
if let text: String = request[keys.sourceText] {
|
|
content = text
|
|
} else if let file: String = request[keys.sourceFile] {
|
|
logger.info("Document open request missing source text. Reading contents of '\(file)' from disk.")
|
|
do {
|
|
content = try String(contentsOfFile: file)
|
|
} catch {
|
|
self.logger.error("error: dropping request editor.open: failed to read \(file): \(String(describing: error))")
|
|
return
|
|
}
|
|
} else {
|
|
self.logger.error("error: dropping request editor.open: missing 'key.sourcetext'")
|
|
return
|
|
}
|
|
|
|
self.connection.openDocument(
|
|
path: path,
|
|
contents: content,
|
|
compilerArguments: request[keys.compilerArgs]?.asStringArray
|
|
)
|
|
}
|
|
|
|
func handleDocumentEdit(_ request: SKDRequestDictionaryReader) {
|
|
let keys = request.sourcekitd.keys
|
|
guard let path: String = request[keys.name] else {
|
|
self.logger.error("error: dropping request editor.replacetext: missing 'key.name'")
|
|
return
|
|
}
|
|
guard let offset: Int = request[keys.offset] else {
|
|
self.logger.error("error: dropping request editor.replacetext: missing 'key.offset'")
|
|
return
|
|
}
|
|
guard let length: Int = request[keys.length] else {
|
|
self.logger.error("error: dropping request editor.replacetext: missing 'key.length'")
|
|
return
|
|
}
|
|
guard let text: String = request[keys.sourceText] else {
|
|
self.logger.error("error: dropping request editor.replacetext: missing 'key.sourcetext'")
|
|
return
|
|
}
|
|
|
|
self.connection.editDocument(path: path, atUTF8Offset: offset, length: length, newText: text)
|
|
}
|
|
|
|
func handleDocumentClose(_ dict: SKDRequestDictionaryReader) {
|
|
guard let path: String = dict[dict.sourcekitd.keys.name] else {
|
|
self.logger.error("error: dropping request editor.close: missing 'key.name'")
|
|
return
|
|
}
|
|
self.connection.closeDocument(path: path)
|
|
}
|
|
|
|
func handleCompleteOpen(
|
|
_ request: SKDRequestDictionaryReader,
|
|
handle: RequestHandle?
|
|
) throws -> SKDResponseDictionaryBuilder {
|
|
let sourcekitd = request.sourcekitd
|
|
let keys = sourcekitd.keys
|
|
let location = try self.requestLocation(request)
|
|
|
|
if self.currentSession != nil {
|
|
logger.error("Opening a code completion session while previous is still open. Implicitly closing old session.")
|
|
self.currentSession = nil
|
|
}
|
|
|
|
let options: SKDRequestDictionaryReader? = request[keys.codeCompleteOptions]
|
|
let annotate = (options?[keys.annotatedDescription] as Int?) == 1
|
|
let includeObjectLiterals = (options?[keys.includeObjectLiterals] as Int?) == 1
|
|
let addInitsToTopLevel = (options?[keys.addInitsToTopLevel] as Int?) == 1
|
|
let addCallWithNoDefaultArgs = (options?[keys.addCallWithNoDefaultArgs] as Int? == 1)
|
|
let includeSemanticComponents = (options?[keys.includeSemanticComponents] as Int?) == 1
|
|
|
|
if let recentCompletions: [String] = options?[keys.recentCompletions]?.asStringArray {
|
|
self.connection.updateRecentCompletions(recentCompletions)
|
|
}
|
|
|
|
let session = try self.connection.complete(
|
|
at: location,
|
|
arguments: request[keys.compilerArgs]?.asStringArray,
|
|
options: CompletionOptions(
|
|
annotateResults: annotate,
|
|
includeObjectLiterals: includeObjectLiterals,
|
|
addInitsToTopLevel: addInitsToTopLevel,
|
|
addCallWithNoDefaultArgs: addCallWithNoDefaultArgs,
|
|
includeSemanticComponents: includeSemanticComponents
|
|
),
|
|
handle: handle?.handle
|
|
)
|
|
|
|
self.currentSession = session
|
|
|
|
return completionsResponse(session: session, options: options, sourcekitd: sourcekitd)
|
|
}
|
|
|
|
func handleCompleteUpdate(_ request: SKDRequestDictionaryReader) throws -> SKDResponseDictionaryBuilder {
|
|
let sourcekitd = request.sourcekitd
|
|
let location = try self.requestLocation(request)
|
|
|
|
let options: SKDRequestDictionaryReader? = request[sourcekitd.keys.codeCompleteOptions]
|
|
|
|
guard let session = self.currentSession, session.location == location else {
|
|
throw GenericPluginError(description: "no matching session for \(location)")
|
|
}
|
|
|
|
return completionsResponse(session: session, options: options, sourcekitd: sourcekitd)
|
|
}
|
|
|
|
func handleCompleteClose(_ dict: SKDRequestDictionaryReader) throws -> SKDResponseDictionaryBuilder {
|
|
let sourcekitd = dict.sourcekitd
|
|
|
|
let location = try self.requestLocation(dict)
|
|
|
|
guard let session = self.currentSession, session.location == location else {
|
|
throw GenericPluginError(description: "no matching session for \(location)")
|
|
}
|
|
|
|
self.currentSession = nil
|
|
return sourcekitd.responseDictionary([:])
|
|
}
|
|
|
|
func handleExtendedCompletionRequest(_ request: SKDRequestDictionaryReader) throws -> ExtendedCompletionInfo {
|
|
let sourcekitd = request.sourcekitd
|
|
let keys = sourcekitd.keys
|
|
|
|
guard let opaqueID: Int64 = request[keys.identifier] else {
|
|
throw InvalidRequest.missingKey("key.identifier")
|
|
}
|
|
|
|
guard let session = self.currentSession else {
|
|
throw GenericPluginError(description: "no matching session for request \(request)")
|
|
}
|
|
|
|
let id = CompletionItem.Identifier(opaqueValue: opaqueID)
|
|
guard let info = session.extendedCompletionInfo(for: id) else {
|
|
throw GenericPluginError(description: "unknown completion \(opaqueID) for session at \(session.location)")
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
func handleCompletionDocumentation(_ request: SKDRequestDictionaryReader) throws -> SKDResponseDictionaryBuilder {
|
|
let info = try handleExtendedCompletionRequest(request)
|
|
|
|
return request.sourcekitd.responseDictionary([
|
|
request.sourcekitd.keys.docBrief: info.briefDocumentation,
|
|
request.sourcekitd.keys.associatedUSRs: info.associatedUSRs as [SKDResponseValue]?,
|
|
])
|
|
}
|
|
|
|
func handleCompletionDiagnostic(_ dict: SKDRequestDictionaryReader) throws -> SKDResponseDictionaryBuilder {
|
|
let info = try handleExtendedCompletionRequest(dict)
|
|
let sourcekitd = dict.sourcekitd
|
|
|
|
let severity: sourcekitd_api_uid_t? =
|
|
switch info.diagnostic?.severity {
|
|
case .note: sourcekitd.values.diagNote
|
|
case .remark: sourcekitd.values.diagRemark
|
|
case .warning: sourcekitd.values.diagWarning
|
|
case .error: sourcekitd.values.diagError
|
|
default: nil
|
|
}
|
|
return sourcekitd.responseDictionary([
|
|
sourcekitd.keys.severity: severity,
|
|
sourcekitd.keys.description: info.diagnostic?.description,
|
|
])
|
|
}
|
|
|
|
func handleDependencyUpdated() {
|
|
connection.markCachedCompilerInstanceShouldBeInvalidated()
|
|
}
|
|
|
|
func handleSetPopularAPI(_ dict: SKDRequestDictionaryReader) -> SKDResponseDictionaryBuilder {
|
|
let sourcekitd = dict.sourcekitd
|
|
let keys = sourcekitd.keys
|
|
|
|
let didUseScoreComponents: Bool
|
|
|
|
// Try 'PopularityIndex' scheme first, then fall back to `PopularityTable`
|
|
// scheme.
|
|
if let scopedPopularityDataPath: String = dict[keys.scopedPopularityTablePath] {
|
|
// NOTE: Currently, the client sends setpopularapi before every
|
|
// 'complete.open' because sourcekit might have crashed before it.
|
|
// 'scoped_popularity_table_path' and its content typically do not
|
|
// change in the 'Connection' lifetime, but 'popular_modules'/'notorious_modules'
|
|
// might. We cache the populated table, and use it as long as the these
|
|
// values are the same as the previous request.
|
|
self.connection.updatePopularityIndex(
|
|
scopedPopularityDataPath: scopedPopularityDataPath,
|
|
popularModules: dict[keys.popularModules]?.asStringArray ?? [],
|
|
notoriousModules: dict[keys.notoriousModules]?.asStringArray ?? []
|
|
)
|
|
didUseScoreComponents = true
|
|
} else if let popularityTable = PopularityTable(dict) {
|
|
self.connection.updatePopularAPI(popularityTable: popularityTable)
|
|
didUseScoreComponents = true
|
|
} else {
|
|
let popular: [String] = dict[keys.popular]?.asStringArray ?? []
|
|
let unpopular: [String] = dict[keys.unpopular]?.asStringArray ?? []
|
|
let popularityTable = PopularityTable(popularSymbols: popular, recentSymbols: [], notoriousSymbols: unpopular)
|
|
self.connection.updatePopularAPI(popularityTable: popularityTable)
|
|
didUseScoreComponents = false
|
|
}
|
|
return sourcekitd.responseDictionary([
|
|
keys.useNewAPI: 1, // Make it possible to detect this was handled by the plugin.
|
|
keys.usedScoreComponents: didUseScoreComponents ? 1 : 0,
|
|
])
|
|
}
|
|
|
|
private func requestLocation(_ dict: SKDRequestDictionaryReader) throws -> Location {
|
|
let keys = dict.sourcekitd.keys
|
|
guard let path: String = dict[keys.sourceFile] else {
|
|
throw InvalidRequest.missingKey("key.sourcefile")
|
|
}
|
|
guard let line: Int = dict[keys.line] else {
|
|
throw InvalidRequest.missingKey("key.line")
|
|
}
|
|
guard let column: Int = dict[keys.column] else {
|
|
throw InvalidRequest.missingKey("key.column")
|
|
}
|
|
return Location(path: path, position: Position(line: line, utf8Column: column))
|
|
}
|
|
|
|
private func populateCompletionsXPC(
|
|
_ completions: [CompletionItem],
|
|
in session: CompletionSession,
|
|
into resp: inout SKDResponseDictionaryBuilder,
|
|
sourcekitd: SourceKitD
|
|
) {
|
|
let keys = sourcekitd.keys
|
|
|
|
let options = session.options
|
|
if options.annotateResults {
|
|
resp.set(keys.annotatedTypeName, to: true)
|
|
}
|
|
|
|
let results =
|
|
completions.map { item in
|
|
sourcekitd.responseDictionary([
|
|
keys.kind: sourcekitd_api_uid_t(item.kind, sourcekitd: sourcekitd),
|
|
keys.identifier: item.id.opaqueValue,
|
|
keys.name: item.filterText,
|
|
keys.description: item.label,
|
|
keys.sourceText: item.textEdit.newText,
|
|
keys.isSystem: item.isSystem ? 1 : 0,
|
|
keys.numBytesToErase: item.numBytesToErase(from: session.location.position),
|
|
keys.typeName: item.typeName ?? "", // FIXME: make it optional?
|
|
keys.textMatchScore: item.textMatchScore,
|
|
keys.semanticScore: item.semanticScore,
|
|
keys.semanticScoreComponents: options.includeSemanticComponents ? nil : item.semanticClassification?.asBase64,
|
|
keys.priorityBucket: item.priorityBucket.rawValue,
|
|
keys.hasDiagnostic: item.hasDiagnostic ? 1 : 0,
|
|
keys.groupId: item.groupID,
|
|
])
|
|
} as [SKDResponseValue]
|
|
resp.set(sourcekitd.keys.results, to: results)
|
|
}
|
|
|
|
private func populateCompletions(
|
|
_ completions: [CompletionItem],
|
|
in session: CompletionSession,
|
|
into resp: inout SKDResponseDictionaryBuilder,
|
|
includeSemanticComponents: Bool,
|
|
sourcekitd: SourceKitD
|
|
) {
|
|
let keys = sourcekitd.keys
|
|
|
|
let options = session.options
|
|
if options.annotateResults {
|
|
resp.set(keys.annotatedTypeName, to: true)
|
|
}
|
|
|
|
var builder = CompletionResultsArrayBuilder(
|
|
bufferKind: self.completionResultsBufferKind,
|
|
numResults: completions.count,
|
|
session: session
|
|
)
|
|
for item in completions {
|
|
builder.add(item, includeSemanticComponents: includeSemanticComponents, sourcekitd: sourcekitd)
|
|
}
|
|
|
|
let bytes = builder.bytes()
|
|
bytes.withUnsafeBytes { buffer in
|
|
resp.set(keys.results, toCustomBuffer: buffer)
|
|
}
|
|
}
|
|
|
|
private func completionsResponse(
|
|
session: CompletionSession,
|
|
options: SKDRequestDictionaryReader?,
|
|
sourcekitd: SourceKitD
|
|
) -> SKDResponseDictionaryBuilder {
|
|
let keys = sourcekitd.keys
|
|
var response = sourcekitd.responseDictionary([
|
|
keys.unfilteredResultCount: session.totalCount,
|
|
keys.memberAccessTypes: session.memberAccessTypes as [SKDResponseValue],
|
|
])
|
|
|
|
let filterText = options?[keys.filterText] ?? ""
|
|
let maxResults = CompletionOptions.maxResults(input: options?[keys.maxResults])
|
|
let includeSemanticComponents = (options?[keys.includeSemanticComponents] as Int?) == 1
|
|
|
|
let completions = session.completions(matchingFilterText: filterText, maxResults: maxResults)
|
|
|
|
if let useXPC: Int = options?[keys.useXPCSerialization], useXPC != 0 {
|
|
self.populateCompletionsXPC(completions, in: session, into: &response, sourcekitd: sourcekitd)
|
|
} else {
|
|
self.populateCompletions(
|
|
completions,
|
|
in: session,
|
|
into: &response,
|
|
includeSemanticComponents: includeSemanticComponents,
|
|
sourcekitd: sourcekitd
|
|
)
|
|
}
|
|
return response
|
|
}
|
|
}
|
|
|
|
extension sourcekitd_api_uid_t {
|
|
init(_ itemKind: CompletionItem.ItemKind, isRef: Bool = false, sourcekitd: SourceKitD) {
|
|
switch itemKind {
|
|
case .module:
|
|
self = isRef ? sourcekitd.values.refModule : sourcekitd.values.declModule
|
|
case .class:
|
|
self = isRef ? sourcekitd.values.refClass : sourcekitd.values.declClass
|
|
case .actor:
|
|
self = isRef ? sourcekitd.values.refActor : sourcekitd.values.declActor
|
|
case .struct:
|
|
self = isRef ? sourcekitd.values.refStruct : sourcekitd.values.declStruct
|
|
case .enum:
|
|
self = isRef ? sourcekitd.values.refEnum : sourcekitd.values.declEnum
|
|
case .enumElement:
|
|
self = isRef ? sourcekitd.values.refEnumElement : sourcekitd.values.declEnumElement
|
|
case .protocol:
|
|
self = isRef ? sourcekitd.values.refProtocol : sourcekitd.values.declProtocol
|
|
case .associatedType:
|
|
self = isRef ? sourcekitd.values.refAssociatedType : sourcekitd.values.declAssociatedType
|
|
case .typeAlias:
|
|
self = isRef ? sourcekitd.values.refTypeAlias : sourcekitd.values.declTypeAlias
|
|
case .genericTypeParam:
|
|
self = isRef ? sourcekitd.values.refGenericTypeParam : sourcekitd.values.declGenericTypeParam
|
|
case .constructor:
|
|
self = isRef ? sourcekitd.values.refConstructor : sourcekitd.values.declConstructor
|
|
case .destructor:
|
|
self = isRef ? sourcekitd.values.refDestructor : sourcekitd.values.declDestructor
|
|
case .subscript:
|
|
self = isRef ? sourcekitd.values.refSubscript : sourcekitd.values.declSubscript
|
|
case .staticMethod:
|
|
self = isRef ? sourcekitd.values.refMethodStatic : sourcekitd.values.declMethodStatic
|
|
case .instanceMethod:
|
|
self = isRef ? sourcekitd.values.refMethodInstance : sourcekitd.values.declMethodInstance
|
|
case .prefixOperatorFunction:
|
|
self = isRef ? sourcekitd.values.refFunctionPrefixOperator : sourcekitd.values.declFunctionPrefixOperator
|
|
case .postfixOperatorFunction:
|
|
self = isRef ? sourcekitd.values.refFunctionPostfixOperator : sourcekitd.values.declFunctionPostfixOperator
|
|
case .infixOperatorFunction:
|
|
self = isRef ? sourcekitd.values.refFunctionInfixOperator : sourcekitd.values.declFunctionInfixOperator
|
|
case .freeFunction:
|
|
self = isRef ? sourcekitd.values.refFunctionFree : sourcekitd.values.declFunctionFree
|
|
case .staticVar:
|
|
self = isRef ? sourcekitd.values.refVarStatic : sourcekitd.values.declVarStatic
|
|
case .instanceVar:
|
|
self = isRef ? sourcekitd.values.refVarInstance : sourcekitd.values.declVarInstance
|
|
case .localVar:
|
|
self = isRef ? sourcekitd.values.refVarLocal : sourcekitd.values.declVarLocal
|
|
case .globalVar:
|
|
self = isRef ? sourcekitd.values.refVarGlobal : sourcekitd.values.declVarGlobal
|
|
case .precedenceGroup:
|
|
self = isRef ? sourcekitd.values.refPrecedenceGroup : sourcekitd.values.declPrecedenceGroup
|
|
case .macro:
|
|
self = isRef ? sourcekitd.values.refMacro : sourcekitd.values.declMacro
|
|
case .keyword:
|
|
self = sourcekitd.values.completionKindKeyword
|
|
case .operator:
|
|
// FIXME: special operator ?
|
|
self = sourcekitd.values.completionKindPattern
|
|
case .literal:
|
|
// FIXME: special literal ?
|
|
self = sourcekitd.values.completionKindKeyword
|
|
case .pattern:
|
|
self = sourcekitd.values.completionKindPattern
|
|
case .unknown:
|
|
// FIXME: special unknown ?
|
|
self = sourcekitd.values.completionKindKeyword
|
|
}
|
|
}
|
|
}
|