mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
I feel like the implementations are actually simpler if we split them. This will also allow us to add more advanced logic to the JSON compilation database build system in the future, such as inferring the toolchain from the compile command.
340 lines
12 KiB
Swift
340 lines
12 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 SKUtilities
|
|
import SourceKitD
|
|
import SwiftExtensions
|
|
|
|
extension PopularityIndex.Scope {
|
|
init(string name: String) {
|
|
if let dotIndex = name.firstIndex(of: ".") {
|
|
self.init(
|
|
container: String(name[name.index(after: dotIndex)...]),
|
|
module: String(name[..<dotIndex])
|
|
)
|
|
} else {
|
|
self.init(container: nil, module: name)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Execute the given block on a thread with the given stack size and wait for that thread to finish.
|
|
fileprivate func withStackSize<T>(_ stackSize: Int, execute block: @Sendable @escaping () -> T) -> T {
|
|
var result: T! = nil
|
|
nonisolated(unsafe) let workItem = DispatchWorkItem(block: {
|
|
result = block()
|
|
})
|
|
let thread = Thread {
|
|
workItem.perform()
|
|
}
|
|
thread.stackSize = stackSize
|
|
thread.start()
|
|
workItem.wait()
|
|
return result!
|
|
}
|
|
|
|
final class Connection {
|
|
enum Error: SourceKitPluginError, CustomStringConvertible {
|
|
case openingFileFailed(path: String)
|
|
/// An error that occurred inside swiftIDE while performing completion.
|
|
case swiftIDEError(String)
|
|
case cancelled
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .openingFileFailed(path: let path):
|
|
return "Could not open file '\(path)'"
|
|
case .swiftIDEError(let message):
|
|
return message
|
|
case .cancelled:
|
|
return "Request cancelled"
|
|
}
|
|
}
|
|
|
|
func response(sourcekitd: any SourceKitD) -> SKDResponse {
|
|
switch self {
|
|
case .openingFileFailed, .swiftIDEError:
|
|
return SKDResponse(error: .failed, description: description, sourcekitd: sourcekitd)
|
|
case .cancelled:
|
|
return SKDResponse(error: .cancelled, description: "Request cancelled", sourcekitd: sourcekitd)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate let logger = Logger(subsystem: "org.swift.sourcekit.service-plugin", category: "Connection")
|
|
|
|
private let impl: swiftide_api_connection_t
|
|
let sourcekitd: SourceKitD
|
|
|
|
/// The list of documents that are open in SourceKitD. The key is the file's path on disk or a pseudo-path that
|
|
/// uniquely identifies the document if it doesn't exist on disk.
|
|
private var documents: [String: Document] = [:]
|
|
|
|
/// Information to construct `PopularityIndex`.
|
|
private var scopedPopularityDataPath: String?
|
|
private var popularModules: [String]?
|
|
private var notoriousModules: [String]?
|
|
|
|
/// Cached data read from `scopedPopularityDataPath`.
|
|
private var _scopedPopularityData: LazyValue<[PopularityIndex.Scope: [String: Double]]?> = .uninitialized
|
|
|
|
/// Cached index.
|
|
private var _popularityIndex: LazyValue<PopularityIndex?> = .uninitialized
|
|
|
|
/// Deprecated.
|
|
/// NOTE: `PopularityTable` was replaced with `PopularityIndex`. We keep this
|
|
/// until all clients migrates to `PopularityIndex`.
|
|
private var onlyPopularCompletions: PopularityTable = .init()
|
|
|
|
/// Recent completions that were accepted by the client.
|
|
private var recentCompletions: [String] = []
|
|
|
|
/// The stack size that should be used for all operations that end up invoking the type checker.
|
|
private let semanticStackSize = 8 << 20 // 8 MB.
|
|
|
|
init(opaqueIDEInspectionInstance: UnsafeMutableRawPointer?, sourcekitd: SourceKitD) {
|
|
self.sourcekitd = sourcekitd
|
|
impl = sourcekitd.ideApi.connection_create_with_inspection_instance(opaqueIDEInspectionInstance)
|
|
}
|
|
|
|
deinit {
|
|
sourcekitd.ideApi.connection_dispose(impl)
|
|
}
|
|
|
|
//// A function that can be called to cancel a request with a request.
|
|
///
|
|
/// This is not a member function on `Connection` so that `CompletionProvider` can store
|
|
/// this closure in a member and call it even while the `CompletionProvider` actor is busy
|
|
/// fulfilling a completion request and thus can't access `connection`.
|
|
var cancellationFunc: @Sendable (RequestHandle) -> Void {
|
|
nonisolated(unsafe) let impl = self.impl
|
|
return { [sourcekitd] handle in
|
|
sourcekitd.ideApi.cancel_request(impl, handle.handle)
|
|
}
|
|
}
|
|
|
|
func openDocument(path: String, contents: String, compilerArguments: [String]? = nil) {
|
|
if documents[path] != nil {
|
|
logger.error("Document at '\(path)' is already open")
|
|
}
|
|
documents[path] = Document(contents: contents, compilerArguments: compilerArguments)
|
|
sourcekitd.ideApi.set_file_contents(impl, path, contents)
|
|
}
|
|
|
|
func editDocument(path: String, atUTF8Offset offset: Int, length: Int, newText: String) {
|
|
guard let document = documents[path] else {
|
|
logger.error("Document at '\(path)' is not open")
|
|
return
|
|
}
|
|
|
|
document.lineTable.replace(utf8Offset: offset, length: length, with: newText)
|
|
|
|
sourcekitd.ideApi.set_file_contents(impl, path, document.lineTable.content)
|
|
}
|
|
|
|
func editDocument(path: String, edit: TextEdit) {
|
|
guard let document = documents[path] else {
|
|
logger.error("Document at '\(path)' is not open")
|
|
return
|
|
}
|
|
|
|
document.lineTable.replace(
|
|
fromLine: edit.range.lowerBound.line - 1,
|
|
utf8Offset: edit.range.lowerBound.utf8Column - 1,
|
|
toLine: edit.range.upperBound.line - 1,
|
|
utf8Offset: edit.range.upperBound.utf8Column - 1,
|
|
with: edit.newText
|
|
)
|
|
|
|
sourcekitd.ideApi.set_file_contents(impl, path, document.lineTable.content)
|
|
}
|
|
|
|
func closeDocument(path: String) {
|
|
if documents[path] == nil {
|
|
logger.error("Document at '\(path)' was not open")
|
|
}
|
|
documents[path] = nil
|
|
sourcekitd.ideApi.set_file_contents(impl, path, nil)
|
|
}
|
|
|
|
func complete(
|
|
at loc: Location,
|
|
arguments reqArgs: [String]? = nil,
|
|
options: CompletionOptions,
|
|
handle: swiftide_api_request_handle_t?
|
|
) throws -> CompletionSession {
|
|
let offset: Int = try {
|
|
if let lineTable = documents[loc.path]?.lineTable {
|
|
return lineTable.utf8OffsetOf(line: loc.line - 1, utf8Column: loc.utf8Column - 1)
|
|
} else {
|
|
// FIXME: move line:column translation into C++ impl. so that we can avoid reading the file an extra time here.
|
|
do {
|
|
logger.log("Received code completion request for file that wasn't open. Reading file contents from disk.")
|
|
let contents = try String(contentsOfFile: loc.path)
|
|
let lineTable = LineTable(contents)
|
|
return lineTable.utf8OffsetOf(line: loc.line - 1, utf8Column: loc.utf8Column - 1)
|
|
} catch {
|
|
throw Error.openingFileFailed(path: loc.path)
|
|
}
|
|
}
|
|
}()
|
|
|
|
let arguments = reqArgs ?? documents[loc.path]?.compilerArguments ?? []
|
|
|
|
let result: swiftide_api_completion_response_t = withArrayOfCStrings(arguments) { cargs in
|
|
let req = sourcekitd.ideApi.completion_request_create(loc.path, UInt32(offset), cargs, UInt32(cargs.count))
|
|
defer { sourcekitd.ideApi.completion_request_dispose(req) }
|
|
sourcekitd.ideApi.completion_request_set_annotate_result(req, options.annotateResults)
|
|
sourcekitd.ideApi.completion_request_set_include_objectliterals(req, options.includeObjectLiterals);
|
|
sourcekitd.ideApi.completion_request_set_add_inits_to_top_level(req, options.addInitsToTopLevel);
|
|
sourcekitd.ideApi.completion_request_set_add_call_with_no_default_args(req, options.addCallWithNoDefaultArgs);
|
|
|
|
do {
|
|
let sourcekitd = self.sourcekitd
|
|
nonisolated(unsafe) let impl = impl
|
|
nonisolated(unsafe) let req = req
|
|
nonisolated(unsafe) let handle = handle
|
|
return withStackSize(semanticStackSize) {
|
|
sourcekitd.ideApi.complete_cancellable(impl, req, handle)!
|
|
}
|
|
}
|
|
}
|
|
|
|
if sourcekitd.ideApi.completion_result_is_error(result) {
|
|
let errorDescription = String(cString: sourcekitd.ideApi.completion_result_get_error_description(result)!)
|
|
// Usually `CompletionSession` is responsible for disposing the result.
|
|
// Since we don't form a `CompletionSession`, dispose of the result manually.
|
|
sourcekitd.ideApi.completion_result_dispose(result)
|
|
throw Error.swiftIDEError(errorDescription)
|
|
} else if sourcekitd.ideApi.completion_result_is_cancelled(result) {
|
|
sourcekitd.ideApi.completion_result_dispose(result)
|
|
throw Error.cancelled
|
|
}
|
|
|
|
return CompletionSession(
|
|
connection: self,
|
|
location: loc,
|
|
response: result,
|
|
options: options
|
|
)
|
|
}
|
|
|
|
func markCachedCompilerInstanceShouldBeInvalidated() {
|
|
sourcekitd.ideApi.connection_mark_cached_compiler_instance_should_be_invalidated(impl, nil)
|
|
}
|
|
|
|
// MARK: 'PopularityIndex' APIs.
|
|
|
|
func updatePopularityIndex(
|
|
scopedPopularityDataPath: String,
|
|
popularModules: [String],
|
|
notoriousModules: [String]
|
|
) {
|
|
|
|
// Clear the cache if necessary.
|
|
// We don't check the content of the path assuming it's not changed.
|
|
// For 'popular/notoriousModules', we expect around 200 elements.
|
|
if scopedPopularityDataPath != self.scopedPopularityDataPath {
|
|
self._popularityIndex = .uninitialized
|
|
self._scopedPopularityData = .uninitialized
|
|
} else if popularModules != self.popularModules || notoriousModules != self.notoriousModules {
|
|
self._popularityIndex = .uninitialized
|
|
}
|
|
|
|
self.scopedPopularityDataPath = scopedPopularityDataPath
|
|
self.popularModules = popularModules
|
|
self.notoriousModules = notoriousModules
|
|
}
|
|
|
|
private var scopedPopularityData: [PopularityIndex.Scope: [String: Double]]? {
|
|
_scopedPopularityData.cachedValueOrCompute {
|
|
guard let jsonPath = self.scopedPopularityDataPath else {
|
|
return nil
|
|
}
|
|
|
|
// A codable representation of `PopularityIndex.symbolPopularity`.
|
|
struct ScopedSymbolPopularity: Codable {
|
|
let values: [String]
|
|
let scores: [Double]
|
|
|
|
var table: [String: Double] {
|
|
var map = [String: Double]()
|
|
for (value, score) in zip(values, scores) {
|
|
map[value] = score
|
|
}
|
|
return map
|
|
}
|
|
}
|
|
|
|
do {
|
|
let jsonURL = URL(fileURLWithPath: jsonPath)
|
|
let decoder = JSONDecoder()
|
|
let data = try Data(contentsOf: jsonURL)
|
|
let decoded = try decoder.decode([String: ScopedSymbolPopularity].self, from: data)
|
|
var result = [PopularityIndex.Scope: [String: Double]]()
|
|
for (rawScope, popularity) in decoded {
|
|
let scope = PopularityIndex.Scope(string: rawScope)
|
|
result[scope] = popularity.table
|
|
}
|
|
return result
|
|
} catch {
|
|
logger.error("Failed to read popularity data at '\(jsonPath)'")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
var popularityIndex: PopularityIndex? {
|
|
_popularityIndex.cachedValueOrCompute {
|
|
guard let scopedPopularityData, let popularModules, let notoriousModules else {
|
|
return nil
|
|
}
|
|
return PopularityIndex(
|
|
symbolReferencePercentages: scopedPopularityData,
|
|
notoriousSymbols: /*unused*/ [],
|
|
popularModules: popularModules,
|
|
notoriousModules: notoriousModules
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: 'PopularityTable' APIs (DEPRECATED).
|
|
|
|
func updatePopularAPI(popularityTable: PopularityTable) {
|
|
self.onlyPopularCompletions = popularityTable
|
|
}
|
|
|
|
func updateRecentCompletions(_ recent: [String]) {
|
|
self.recentCompletions = recent
|
|
}
|
|
|
|
var popularityTable: PopularityTable {
|
|
var result = onlyPopularCompletions
|
|
result.add(popularSymbols: recentCompletions)
|
|
return result
|
|
}
|
|
}
|
|
|
|
private final class Document {
|
|
var lineTable: LineTable
|
|
var compilerArguments: [String]? = nil
|
|
|
|
init(contents: String, compilerArguments: [String]? = nil) {
|
|
self.lineTable = LineTable(contents)
|
|
self.compilerArguments = compilerArguments
|
|
}
|
|
}
|