mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
307 lines
11 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|