Files
sourcekit-lsp/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift
2025-10-31 14:11:11 -07:00

307 lines
11 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
@_spi(SourceKitLSP) import SKLogging
import SourceKitD
/// Represents a code completion session.
///
/// A code completion session is code completion invoked at a specific location. We might filter results as the user
/// types more characters but the fundamental set of results doesn't change during the session. Invoking code completion
/// at a different location or making an edit to the source file that doesn't filter the code completion results should
/// start a new completion session.
final class CompletionSession {
/// The connection to sourcekitd from which we get the raw set of results.
private let connection: Connection
/// The location at which code completion was invoked.
let location: Location
/// A handle to the set of results for this session in sourcekitd. This allows us to retrieve additional information
/// for each code completion item from sourcekitd.
let response: swiftide_api_completion_response_t
/// The list of code completion items available in this session, without any filters applied.
let items: [ASTCompletionItem]
/// The filter names for all code completion items in a `CandidateBatch`, which is used for sorting.
let filterCandidates: CandidateBatch
/// Information about popular symbols to influence scoring.
private let popularityIndex: PopularityIndex?
private let popularityTable: PopularityTable?
/// Information about the code completion session that applies to all completion items, like what kind of completion
/// we are performing (member completion, global completion, ...).
private let context: CompletionContext
/// Completion options that were set by client when the code completion session was opened.
let options: CompletionOptions
/// Convenience accessor to the `SourceKitD` instance.
var sourcekitd: SourceKitD { connection.sourcekitd }
var logger: Logger { connection.logger }
init(
connection: Connection,
location: Location,
response: swiftide_api_completion_response_t,
options: CompletionOptions
) {
let sourcekitd = connection.sourcekitd
self.connection = connection
self.location = location
self.response = response
self.options = options
self.popularityIndex = connection.popularityIndex
self.popularityTable = connection.popularityTable
let completionKind = CompletionContext.Kind(connection.sourcekitd.ideApi.completion_result_get_kind(response))
var memberAccessTypes: [String] = []
sourcekitd.ideApi.completion_result_foreach_baseexpr_typename(response) { charPtr in
memberAccessTypes.append(String(cString: charPtr!))
return false
}
var baseExprScope: PopularityIndex.Scope? = nil
if let popularityIndex = popularityIndex {
// Use the first scope found in 'popularityIndex'.
for typeName in memberAccessTypes {
let scope = PopularityIndex.Scope(string: typeName)
if popularityIndex.isKnownScope(scope) {
baseExprScope = scope
break
}
}
}
let context = CompletionContext(
kind: completionKind,
memberAccessTypes: memberAccessTypes,
baseExprScope: baseExprScope
)
self.context = context
var candidateStrings: [String] = []
var items: [ASTCompletionItem] = []
sourcekitd.ideApi.completion_result_get_completions(response) { itemsPtr, filterPtr, numItems in
items.reserveCapacity(Int(numItems))
candidateStrings.reserveCapacity(Int(numItems))
let citems = UnsafeBufferPointer(start: itemsPtr, count: Int(numItems))
let cfilters = UnsafeBufferPointer(start: filterPtr, count: Int(numItems))
for i in 0..<Int(numItems) {
let citem = citems[i]
let cfilter = cfilters[i]
let item = ASTCompletionItem(
citem!,
filterName: cfilter,
completionKind: context.kind,
index: UInt32(i),
sourcekitd: sourcekitd
)
candidateStrings.append(item.filterName)
items.append(item)
}
}
self.items = items
self.filterCandidates = CandidateBatch(candidates: candidateStrings, contentType: .codeCompletionSymbol)
precondition(items.count == filterCandidates.count)
}
var totalCount: Int {
return items.count
}
func completions(matchingFilterText filterText: String, maxResults: Int) -> [CompletionItem] {
let sorting = CompletionSorting(filterText: filterText, in: self)
let range =
location.position..<Position(line: location.line, utf8Column: location.utf8Column + filterText.utf8.count)
return sorting.withScoredAndFilter(maxResults: maxResults) { (matches) -> [CompletionItem] in
var nextGroupId = 1 // NOTE: Never use zero. 0 can be considered null groupID.
var baseNameToGroupId: [String: Int] = [:]
return matches.map {
CompletionItem(
items[$0.index],
score: $0.score,
in: self,
completionReplaceRange: range,
groupID: { (baseName: String) -> Int in
if let entry = baseNameToGroupId[baseName] {
return entry
} else {
let groupId = nextGroupId
baseNameToGroupId[baseName] = groupId
nextGroupId += 1
return groupId
}
}
)
}
}
}
deinit {
sourcekitd.ideApi.completion_result_dispose(response)
}
func popularity(ofSymbol name: String, inModule module: String?) -> Popularity? {
guard let popularityIndex = self.popularityIndex else {
// Fall back to deprecated 'popularityTable'.
if let popularityTable = self.popularityTable {
return popularityTable.popularity(symbol: name, module: module)
}
return nil
}
let shouldUseBaseExprScope: Bool
// Use the base expression scope, for member completions.
switch completionKind {
case .dotExpr, .unresolvedMember, .postfixExpr, .keyPathExprSwift, .keyPathExprObjC:
shouldUseBaseExprScope = true
default:
// FIXME: 'baseExprScope' might still be populated for implicit self
// members. e.g. global expression completion in a method.
// We might want to use `baseExprScope` if the symbol is a type member.
shouldUseBaseExprScope = false
}
let scope: PopularityIndex.Scope
// 'baseExprScope == nil' means the 'PopularityIndex' doesn't know the scope.
// Fallback to the symbol module scope.
if shouldUseBaseExprScope, let baseExprScope = context.baseExprScope {
scope = baseExprScope
} else {
guard let module = module else {
// Keywords, etc. don't belong to any module.
return nil
}
scope = PopularityIndex.Scope(container: nil, module: module)
}
// Extract the base name from 'name'.
let baseName: String
if let parenIdx = name.firstIndex(of: "(") {
baseName = String(name[..<parenIdx])
} else {
baseName = name
}
return popularityIndex.popularity(of: PopularityIndex.Symbol(name: baseName, scope: scope))
}
func extendedCompletionInfo(for id: CompletionItem.Identifier) -> ExtendedCompletionInfo? {
return ExtendedCompletionInfo(session: self, index: Int(id.index))
}
var completionKind: CompletionContext.Kind { context.kind }
var memberAccessTypes: [String] { context.memberAccessTypes }
}
/// Information about code completion items that is not returned to the client with the initial results but that the
/// client needs to request for each item with a separate request. It is intended that the client only requests this
/// information when more information about a code completion items should be displayed, eg. because the user selected
/// it.
struct ExtendedCompletionInfo {
private let session: CompletionSession
/// The index of the item to get extended information for in `session.items`.
private let index: Int
private var rawItem: swiftide_api_completion_item_t { session.items[index].impl }
init(session: CompletionSession, index: Int) {
self.session = session
self.index = index
}
var briefDocumentation: String? {
var result: String? = nil
session.sourcekitd.ideApi.completion_item_get_doc_brief(session.response, rawItem) {
if let cstr = $0 {
result = String(cString: cstr)
}
}
return result
}
var fullDocumentationAsXML: String? {
var result: String? = nil
session.sourcekitd.ideApi.completion_item_get_doc_full_as_xml?(session.response, rawItem) {
if let cstr = $0 {
result = String(cString: cstr)
}
}
return result
}
var rawDocumentation: String? {
var result: String? = nil
session.sourcekitd.ideApi.completion_item_get_doc_raw?(session.response, rawItem) {
if let cstr = $0 {
result = String(cString: cstr)
}
}
return result
}
var associatedUSRs: [String] {
var result: [String] = []
session.sourcekitd.ideApi.completion_item_get_associated_usrs(session.response, rawItem) { ptr, len in
result.reserveCapacity(Int(len))
for usr in UnsafeBufferPointer(start: ptr, count: Int(len)) {
if let cstr = usr {
result.append(String(cString: cstr))
}
}
}
return result
}
var diagnostic: CompletionItem.Diagnostic? {
var result: CompletionItem.Diagnostic? = nil
session.sourcekitd.ideApi.completion_item_get_diagnostic(session.response, rawItem) { severity, message in
if let severity = CompletionItem.Diagnostic.Severity(severity) {
result = .init(severity: severity, description: String(cString: message!))
}
}
return result
}
}
extension CompletionItem.Diagnostic.Severity {
init?(_ ideValue: swiftide_api_completion_diagnostic_severity_t) {
switch ideValue {
case SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_ERROR:
self = .error
case SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_WARNING:
self = .warning
case SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_REMARK:
self = .remark
case SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_NOTE:
self = .note
case SWIFTIDE_COMPLETION_DIAGNOSTIC_SEVERITY_NONE:
return nil
default:
// FIXME: Handle unknown severity?
return nil
}
}
}