//===----------------------------------------------------------------------===// // // 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.. [CompletionItem] { let sorting = CompletionSorting(filterText: filterText, in: self) let range = location.position.. [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[.. 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 } } }