Skip to content

Add a SourceKit plugin to handle code completion requests #1906

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 18 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add a SourceKit plugin to handle code completion requests
This adds a sourcekitd plugin that drives the code completion requests. It also includes a `CompletionScoring` module that’s used to rank code completion results based on their contextual match, allowing us to show more relevant code completion results at the top.
  • Loading branch information
ahoppen committed Jan 3, 2025
commit 5709e1a864c0e7b17c60e0f149084a2fede0c17c
163 changes: 163 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ var products: [Product] = [
.executable(name: "sourcekit-lsp", targets: ["sourcekit-lsp"]),
.library(name: "_SourceKitLSP", targets: ["SourceKitLSP"]),
.library(name: "LSPBindings", targets: ["LanguageServerProtocol", "LanguageServerProtocolJSONRPC"]),
.library(name: "SwiftSourceKitPlugin", type: .dynamic, targets: ["SwiftSourceKitPlugin"]),
.library(name: "SwiftSourceKitClientPlugin", type: .dynamic, targets: ["SwiftSourceKitClientPlugin"]),
]

var targets: [Target] = [
Expand Down Expand Up @@ -109,6 +111,40 @@ var targets: [Target] = [
dependencies: []
),

// MARK: CompletionScoring

.target(
name: "CompletionScoring",
dependencies: [],
swiftSettings: globalSwiftSettings
),

.target(
name: "CompletionScoringForPlugin",
dependencies: [],
swiftSettings: globalSwiftSettings
),

.testTarget(
name: "CompletionScoringTests",
dependencies: ["CompletionScoring", "CompletionScoringTestSupport", "SwiftExtensions"],
swiftSettings: globalSwiftSettings
),

.testTarget(
name: "CompletionScoringPerfTests",
dependencies: ["CompletionScoring", "CompletionScoringTestSupport", "SwiftExtensions"],
swiftSettings: globalSwiftSettings
),

// MARK: CompletionScoringTestSupport

.target(
name: "CompletionScoringTestSupport",
dependencies: ["CompletionScoring", "SwiftExtensions"],
swiftSettings: globalSwiftSettings
),

// MARK: CSKTestSupport

.target(
Expand Down Expand Up @@ -273,6 +309,21 @@ var targets: [Target] = [
swiftSettings: globalSwiftSettings + lspLoggingSwiftSettings
),

.target(
name: "SKLoggingForPlugin",
dependencies: [
"SwiftExtensionsForPlugin"
],
exclude: ["CMakeLists.txt"],
swiftSettings: globalSwiftSettings + lspLoggingSwiftSettings + [
// We can't depend on swift-crypto in the plugin because we can't module-alias it due to https://github.com/swiftlang/swift-package-manager/issues/8119
.define("NO_CRYPTO_DEPENDENCY"),
.unsafeFlags([
"-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin",
]),
]
),

.testTarget(
name: "SKLoggingTests",
dependencies: [
Expand Down Expand Up @@ -308,6 +359,21 @@ var targets: [Target] = [
swiftSettings: globalSwiftSettings
),

.target(
name: "SKUtilitiesForPlugin",
dependencies: [
"SKLoggingForPlugin",
"SwiftExtensionsForPlugin",
],
exclude: ["CMakeLists.txt"],
swiftSettings: globalSwiftSettings + [
.unsafeFlags([
"-module-alias", "SKLogging=SKLoggingForPlugin",
"-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin",
])
]
),

.testTarget(
name: "SKUtilitiesTests",
dependencies: [
Expand Down Expand Up @@ -354,6 +420,22 @@ var targets: [Target] = [
swiftSettings: globalSwiftSettings
),

.target(
name: "SourceKitDForPlugin",
dependencies: [
"Csourcekitd",
"SKLoggingForPlugin",
"SwiftExtensionsForPlugin",
],
exclude: ["CMakeLists.txt", "sourcekitd_uids.swift.gyb"],
swiftSettings: globalSwiftSettings + [
.unsafeFlags([
"-module-alias", "SKLogging=SKLoggingForPlugin",
"-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin",
])
]
),

.testTarget(
name: "SourceKitDTests",
dependencies: [
Expand Down Expand Up @@ -429,6 +511,13 @@ var targets: [Target] = [
swiftSettings: globalSwiftSettings
),

.target(
name: "SwiftExtensionsForPlugin",
dependencies: ["CAtomics"],
exclude: ["CMakeLists.txt"],
swiftSettings: globalSwiftSettings
),

.testTarget(
name: "SwiftExtensionsTests",
dependencies: [
Expand All @@ -439,6 +528,80 @@ var targets: [Target] = [
swiftSettings: globalSwiftSettings
),

// MARK: SwiftSourceKitClientPlugin

.target(
name: "SwiftSourceKitClientPlugin",
dependencies: [
"Csourcekitd",
"SourceKitDForPlugin",
"SwiftSourceKitPluginCommon",
],
swiftSettings: globalSwiftSettings + [
.unsafeFlags([
"-module-alias", "SourceKitD=SourceKitDForPlugin",
])
]
),

// MARK: SwiftSourceKitPluginCommon

.target(
name: "SwiftSourceKitPluginCommon",
dependencies: [
"Csourcekitd",
"SourceKitDForPlugin",
"SwiftExtensionsForPlugin",
"SKLoggingForPlugin",
],
swiftSettings: globalSwiftSettings + [
.unsafeFlags([
"-module-alias", "SourceKitD=SourceKitDForPlugin",
"-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin",
"-module-alias", "SKLogging=SKLoggingForPlugin",
])
]
),

// MARK: SwiftSourceKitPlugin

.target(
name: "SwiftSourceKitPlugin",
dependencies: [
"Csourcekitd",
"CompletionScoringForPlugin",
"SKUtilitiesForPlugin",
"SKLoggingForPlugin",
"SourceKitDForPlugin",
"SwiftSourceKitPluginCommon",
"SwiftExtensionsForPlugin",
],
swiftSettings: globalSwiftSettings + [
.unsafeFlags([
"-module-alias", "CompletionScoring=CompletionScoringForPlugin",
"-module-alias", "SKUtilities=SKUtilitiesForPlugin",
"-module-alias", "SourceKitD=SourceKitDForPlugin",
"-module-alias", "SKLogging=SKLoggingForPlugin",
"-module-alias", "SwiftExtensions=SwiftExtensionsForPlugin",
])
]
),

.testTarget(
name: "SwiftSourceKitPluginTests",
dependencies: [
"BuildSystemIntegration",
"CompletionScoring",
"Csourcekitd",
"LanguageServerProtocol",
"SKTestSupport",
"SourceKitD",
"SwiftExtensions",
"ToolchainRegistry",
],
swiftSettings: globalSwiftSettings
),

// MARK: ToolchainRegistry

.target(
Expand Down
75 changes: 75 additions & 0 deletions Sources/CompletionScoring/Semantics/CompletionScore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//===----------------------------------------------------------------------===//
//
// 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 Foundation

/// Represents a composite score formed from a semantic and textual components.
///
/// The textual component forms the bulk of the score typically having a value in the 10's to 100's.
/// You can think of the semantic component as a bonus to the text score.
/// It usually has a value between 0 and 2, and is used as a multiplier.
package struct CompletionScore: Comparable {
package var semanticComponent: Double
package var textComponent: Double

package init(textComponent: Double, semanticComponent: Double) {
self.semanticComponent = semanticComponent
self.textComponent = textComponent
}

package init(textComponent: Double, semanticClassification: SemanticClassification) {
self.semanticComponent = semanticClassification.score
self.textComponent = textComponent
}

package var value: Double {
semanticComponent * textComponent
}

package static func < (_ lhs: Self, _ rhs: Self) -> Bool {
lhs.value < rhs.value
}
}

// MARK: - Deprecated -
extension CompletionScore {
/// There is no natural order to these arguments, so they're alphabetical.
@available(
*,
deprecated,
renamed:
"SemanticClassification(completionKind:deprecationStatus:flair:moduleProximity:popularity:scopeProximity:structuralProximity:synchronicityCompatibility:typeCompatibility:)"
)
package static func semanticScore(
completionKind: CompletionKind,
deprecationStatus: DeprecationStatus,
flair: Flair,
moduleProximity: ModuleProximity,
popularity: Popularity,
scopeProximity: ScopeProximity,
structuralProximity: StructuralProximity,
synchronicityCompatibility: SynchronicityCompatibility,
typeCompatibility: TypeCompatibility
) -> Double {
SemanticClassification(
availability: deprecationStatus,
completionKind: completionKind,
flair: flair,
moduleProximity: moduleProximity,
popularity: popularity,
scopeProximity: scopeProximity,
structuralProximity: structuralProximity,
synchronicityCompatibility: synchronicityCompatibility,
typeCompatibility: typeCompatibility
).score
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//===----------------------------------------------------------------------===//
//
// 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 Foundation

package enum Availability: Equatable {
/// Example: Either not tagged, or explicit availability is compatible with current build context
case available

/// Example: Explicitly unavailable in current build context - ie, only for another platform.
case unavailable

/// Example: deprecated in the future
case softDeprecated

/// Example: deprecated in the present, or past
case deprecated

/// Completion provider doesn't know if the method is deprecated or not
case unknown

/// Example: keyword
case inapplicable

/// Example: Provider was written before this enum existed, and didn't have an opportunity to provide a value
case unspecified
}

extension Availability: BinaryCodable {
package init(_ decoder: inout BinaryDecoder) throws {
self = try decoder.decodeEnumByte { decoder, n in
switch n {
case 0: return .available
case 1: return .unavailable
case 2: return .softDeprecated
case 3: return .deprecated
case 4: return .unknown
case 5: return .inapplicable
case 6: return .unspecified
default: return nil
}
}
}

package func encode(_ encoder: inout BinaryEncoder) {
let value: UInt8
switch self {
case .available: value = 0
case .unavailable: value = 1
case .softDeprecated: value = 2
case .deprecated: value = 3
case .unknown: value = 4
case .inapplicable: value = 5
case .unspecified: value = 6
}
encoder.write(value)
}
}

@available(*, deprecated, renamed: "Availability")
package typealias DeprecationStatus = Availability

extension Availability {
@available(*, deprecated, renamed: "Availability.available")
package static let none = DeprecationStatus.available
}
Loading