Skip to content

Commit d64a4ab

Browse files
authored
Merge pull request #1095 from ahoppen/ahoppen/sort-index-results
Sort results returned by the index
2 parents e9d894c + 0b2ca89 commit d64a4ab

File tree

7 files changed

+250
-97
lines changed

7 files changed

+250
-97
lines changed

Sources/LanguageServerProtocol/SupportTypes/Location.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@
1313
/// Range within a particular document.
1414
///
1515
/// For a location where the document is implied, use `Position` or `Range<Position>`.
16-
public struct Location: ResponseType, Hashable, Codable, CustomDebugStringConvertible {
16+
public struct Location: ResponseType, Hashable, Codable, CustomDebugStringConvertible, Comparable {
17+
public static func < (lhs: Location, rhs: Location) -> Bool {
18+
if lhs.uri != rhs.uri {
19+
return lhs.uri.stringValue < rhs.uri.stringValue
20+
}
21+
if lhs.range.lowerBound != rhs.range.lowerBound {
22+
return lhs.range.lowerBound < rhs.range.lowerBound
23+
}
24+
return lhs.range.upperBound < rhs.range.upperBound
25+
}
1726

1827
public var uri: DocumentURI
1928

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 112 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,7 +1607,7 @@ extension SourceKitServer {
16071607

16081608
/// Find all symbols in the workspace that include a string in their name.
16091609
/// - returns: An array of SymbolOccurrences that match the string.
1610-
func findWorkspaceSymbols(matching: String) -> [SymbolOccurrence] {
1610+
func findWorkspaceSymbols(matching: String) throws -> [SymbolOccurrence] {
16111611
// Ignore short queries since they are:
16121612
// - noisy and slow, since they can match many symbols
16131613
// - normally unintentional, triggered when the user types slowly or if the editor doesn't
@@ -1624,25 +1624,24 @@ extension SourceKitServer {
16241624
subsequence: true,
16251625
ignoreCase: true
16261626
) { symbol in
1627+
if Task.isCancelled {
1628+
return false
1629+
}
16271630
guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else {
16281631
return true
16291632
}
16301633
symbolOccurrenceResults.append(symbol)
1631-
// FIXME: Once we have cancellation support, we should fetch all results and take the top
1632-
// `maxWorkspaceSymbolResults` symbols but bail if cancelled.
1633-
//
1634-
// Until then, take the first `maxWorkspaceSymbolResults` symbols to limit the impact of
1635-
// queries which match many symbols.
1636-
return symbolOccurrenceResults.count < maxWorkspaceSymbolResults
1634+
return true
16371635
}
1636+
try Task.checkCancellation()
16381637
}
1639-
return symbolOccurrenceResults
1638+
return symbolOccurrenceResults.sorted()
16401639
}
16411640

16421641
/// Handle a workspace/symbol request, returning the SymbolInformation.
16431642
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
16441643
func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? {
1645-
let symbols = findWorkspaceSymbols(matching: req.query).map(WorkspaceSymbolItem.init)
1644+
let symbols = try findWorkspaceSymbols(matching: req.query).map(WorkspaceSymbolItem.init)
16461645
return symbols
16471646
}
16481647

@@ -1870,6 +1869,7 @@ extension SourceKitServer {
18701869
}
18711870
return indexToLSPLocation(occurrence.location)
18721871
}
1872+
.sorted()
18731873
}
18741874

18751875
/// Returns the result of a `DefinitionRequest` by running a `SymbolInfoRequest`, inspecting
@@ -1962,18 +1962,19 @@ extension SourceKitServer {
19621962
position: req.position
19631963
)
19641964
)
1965-
guard let symbol = symbols.first,
1966-
let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index
1967-
else {
1965+
guard let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index else {
19681966
return nil
19691967
}
1970-
guard let usr = symbol.usr else { return nil }
1971-
var occurrences = index.occurrences(ofUSR: usr, roles: .baseOf)
1972-
if occurrences.isEmpty {
1973-
occurrences = index.occurrences(relatedToUSR: usr, roles: .overrideOf)
1974-
}
1968+
let locations = symbols.flatMap { (symbol) -> [Location] in
1969+
guard let usr = symbol.usr else { return [] }
1970+
var occurrences = index.occurrences(ofUSR: usr, roles: .baseOf)
1971+
if occurrences.isEmpty {
1972+
occurrences = index.occurrences(relatedToUSR: usr, roles: .overrideOf)
1973+
}
19751974

1976-
return .locations(occurrences.compactMap { indexToLSPLocation($0.location) })
1975+
return occurrences.compactMap { indexToLSPLocation($0.location) }
1976+
}
1977+
return .locations(locations.sorted())
19771978
}
19781979

19791980
func references(
@@ -1987,17 +1988,19 @@ extension SourceKitServer {
19871988
position: req.position
19881989
)
19891990
)
1990-
guard let symbol = symbols.first, let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index
1991-
else {
1991+
guard let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index else {
19921992
return []
19931993
}
1994-
guard let usr = symbol.usr else { return [] }
1995-
logger.info("performing indexed jump-to-def with usr \(usr)")
1996-
var roles: SymbolRole = [.reference]
1997-
if req.context.includeDeclaration {
1998-
roles.formUnion([.declaration, .definition])
1994+
let locations = symbols.flatMap { (symbol) -> [Location] in
1995+
guard let usr = symbol.usr else { return [] }
1996+
logger.info("Finding references for USR \(usr)")
1997+
var roles: SymbolRole = [.reference]
1998+
if req.context.includeDeclaration {
1999+
roles.formUnion([.declaration, .definition])
2000+
}
2001+
return index.occurrences(ofUSR: usr, roles: roles).compactMap { indexToLSPLocation($0.location) }
19992002
}
2000-
return index.occurrences(ofUSR: usr, roles: roles).compactMap { indexToLSPLocation($0.location) }
2003+
return locations.sorted()
20012004
}
20022005

20032006
private func indexToLSPCallHierarchyItem(
@@ -2032,29 +2035,31 @@ extension SourceKitServer {
20322035
position: req.position
20332036
)
20342037
)
2035-
guard let symbol = symbols.first, let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index
2036-
else {
2038+
guard let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index else {
20372039
return nil
20382040
}
20392041
// For call hierarchy preparation we only locate the definition
2040-
guard let usr = symbol.usr else { return nil }
2042+
let usrs = symbols.compactMap(\.usr)
20412043

20422044
// Only return a single call hierarchy item. Returning multiple doesn't make sense because they will all have the
20432045
// same USR (because we query them by USR) and will thus expand to the exact same call hierarchy.
2044-
// Also, VS Code doesn't seem to like multiple call hierarchy items being returned and fails to display any results
2045-
// if they are, failing with `Cannot read properties of undefined (reading 'map')`.
2046-
guard let definition = index.definitionOrDeclarationOccurrences(ofUSR: usr).first else {
2047-
return nil
2048-
}
2049-
guard let location = indexToLSPLocation(definition.location) else {
2050-
return nil
2051-
}
2052-
let callHierarchyItem = self.indexToLSPCallHierarchyItem(
2053-
symbol: definition.symbol,
2054-
moduleName: definition.location.moduleName,
2055-
location: location
2056-
)
2057-
return [callHierarchyItem]
2046+
var callHierarchyItems = usrs.compactMap { (usr) -> CallHierarchyItem? in
2047+
guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
2048+
return nil
2049+
}
2050+
guard let location = indexToLSPLocation(definition.location) else {
2051+
return nil
2052+
}
2053+
return self.indexToLSPCallHierarchyItem(
2054+
symbol: definition.symbol,
2055+
moduleName: definition.location.moduleName,
2056+
location: location
2057+
)
2058+
}.sorted(by: { Location(uri: $0.uri, range: $0.range) < Location(uri: $1.uri, range: $1.range) })
2059+
2060+
// Ideally, we should show multiple symbols. But VS Code fails to display call hierarchies with multiple root items,
2061+
// failing with `Cannot read properties of undefined (reading 'map')`. Pick the first one.
2062+
return Array(callHierarchyItems.prefix(1))
20582063
}
20592064

20602065
/// Extracts our implementation-specific data about a call hierarchy
@@ -2090,7 +2095,7 @@ extension SourceKitServer {
20902095
return occurrence.relations.filter { $0.symbol.kind.isCallable }
20912096
.map { related in
20922097
// Resolve the caller's definition to find its location
2093-
let definition = index.occurrences(ofUSR: related.symbol.usr, roles: [.definition, .declaration]).first
2098+
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: related.symbol.usr)
20942099
let definitionSymbolLocation = definition?.location
20952100
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
20962101

@@ -2104,7 +2109,7 @@ extension SourceKitServer {
21042109
)
21052110
}
21062111
}
2107-
return calls
2112+
return calls.sorted(by: { $0.from.name < $1.from.name })
21082113
}
21092114

21102115
func outgoingCalls(_ req: CallHierarchyOutgoingCallsRequest) async throws -> [CallHierarchyOutgoingCall]? {
@@ -2121,7 +2126,7 @@ extension SourceKitServer {
21212126
}
21222127

21232128
// Resolve the callee's definition to find its location
2124-
let definition = index.occurrences(ofUSR: occurrence.symbol.usr, roles: [.definition, .declaration]).first
2129+
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr)
21252130
let definitionSymbolLocation = definition?.location
21262131
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
21272132

@@ -2134,7 +2139,7 @@ extension SourceKitServer {
21342139
fromRanges: [location.range]
21352140
)
21362141
}
2137-
return calls
2142+
return calls.sorted(by: { $0.to.name < $1.to.name })
21382143
}
21392144

21402145
private func indexToLSPTypeHierarchyItem(
@@ -2153,7 +2158,7 @@ extension SourceKitServer {
21532158
if conformances.isEmpty {
21542159
name = symbol.name
21552160
} else {
2156-
name = "\(symbol.name): \(conformances.map(\.symbol.name).joined(separator: ", "))"
2161+
name = "\(symbol.name): \(conformances.map(\.symbol.name).sorted().joined(separator: ", "))"
21572162
}
21582163
// Add the file name and line to the detail string
21592164
if let url = location.uri.fileURL,
@@ -2197,25 +2202,42 @@ extension SourceKitServer {
21972202
position: req.position
21982203
)
21992204
)
2200-
guard let symbol = symbols.first else {
2205+
guard !symbols.isEmpty else {
22012206
return nil
22022207
}
22032208
guard let index = await self.workspaceForDocument(uri: req.textDocument.uri)?.index else {
22042209
return nil
22052210
}
2206-
guard let usr = symbol.usr else { return nil }
2207-
return index.occurrences(ofUSR: usr, roles: [.definition, .declaration])
2208-
.compactMap { info -> TypeHierarchyItem? in
2209-
guard let location = indexToLSPLocation(info.location) else {
2210-
return nil
2211+
let usrs =
2212+
symbols
2213+
.filter {
2214+
// Only include references to type. For example, we don't want to find the type hierarchy of a constructor when
2215+
// starting the type hierarchy on `Foo()``.
2216+
switch $0.kind {
2217+
case .class, .enum, .interface, .struct: return true
2218+
default: return false
22112219
}
2212-
return self.indexToLSPTypeHierarchyItem(
2213-
symbol: info.symbol,
2214-
moduleName: info.location.moduleName,
2215-
location: location,
2216-
index: index
2217-
)
22182220
}
2221+
.compactMap(\.usr)
2222+
let typeHierarchyItems = usrs.compactMap { (usr) -> TypeHierarchyItem? in
2223+
guard
2224+
let info = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr),
2225+
let location = indexToLSPLocation(info.location)
2226+
else {
2227+
return nil
2228+
}
2229+
return self.indexToLSPTypeHierarchyItem(
2230+
symbol: info.symbol,
2231+
moduleName: info.location.moduleName,
2232+
location: location,
2233+
index: index
2234+
)
2235+
}
2236+
.sorted(by: { $0.name < $1.name })
2237+
2238+
// Ideally, we should show multiple symbols. But VS Code fails to display type hierarchies with multiple root items,
2239+
// failing with `Cannot read properties of undefined (reading 'map')`. Pick the first one.
2240+
return Array(typeHierarchyItems.prefix(1))
22192241
}
22202242

22212243
/// Extracts our implementation-specific data about a type hierarchy
@@ -2249,7 +2271,12 @@ extension SourceKitServer {
22492271
// Resolve retroactive conformances via the extensions
22502272
let extensions = index.occurrences(ofUSR: data.usr, roles: .extendedBy)
22512273
let retroactiveConformanceOccurs = extensions.flatMap { occurrence -> [SymbolOccurrence] in
2252-
guard let related = occurrence.relations.first else {
2274+
if occurrence.relations.count > 1 {
2275+
// When the occurrence has an `extendedBy` relation, it's an extension declaration. An extension can only extend
2276+
// a single type, so there can only be a single relation here.
2277+
logger.fault("Expected at most extendedBy relation but got \(occurrence.relations.count)")
2278+
}
2279+
guard let related = occurrence.relations.sorted().first else {
22532280
return []
22542281
}
22552282
return index.occurrences(relatedToUSR: related.symbol.usr, roles: .baseOf)
@@ -2263,7 +2290,7 @@ extension SourceKitServer {
22632290
}
22642291

22652292
// Resolve the supertype's definition to find its location
2266-
let definition = index.occurrences(ofUSR: occurrence.symbol.usr, roles: [.definition, .declaration]).first
2293+
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr)
22672294
let definitionSymbolLocation = definition?.location
22682295
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
22692296

@@ -2274,7 +2301,7 @@ extension SourceKitServer {
22742301
index: index
22752302
)
22762303
}
2277-
return types
2304+
return types.sorted(by: { $0.name < $1.name })
22782305
}
22792306

22802307
func subtypes(_ req: TypeHierarchySubtypesRequest) async throws -> [TypeHierarchyItem]? {
@@ -2289,14 +2316,19 @@ extension SourceKitServer {
22892316

22902317
// Convert occurrences to type hierarchy items
22912318
let types = occurs.compactMap { occurrence -> TypeHierarchyItem? in
2292-
guard let location = indexToLSPLocation(occurrence.location),
2293-
let related = occurrence.relations.first
2319+
if occurrence.relations.count > 1 {
2320+
// An occurrence with a `baseOf` or `extendedBy` relation is an occurrence inside an inheritance clause.
2321+
// Such an occurrence can only be the source of a single type, namely the one that the inheritance clause belongs
2322+
// to.
2323+
logger.fault("Expected at most extendedBy or baseOf relation but got \(occurrence.relations.count)")
2324+
}
2325+
guard let related = occurrence.relations.sorted().first, let location = indexToLSPLocation(occurrence.location)
22942326
else {
22952327
return nil
22962328
}
22972329

22982330
// Resolve the subtype's definition to find its location
2299-
let definition = index.occurrences(ofUSR: related.symbol.usr, roles: [.definition, .declaration]).first
2331+
let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: related.symbol.usr)
23002332
let definitionSymbolLocation = definition.map(\.location)
23012333
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
23022334

@@ -2307,7 +2339,7 @@ extension SourceKitServer {
23072339
index: index
23082340
)
23092341
}
2310-
return types
2342+
return types.sorted { $0.name < $1.name }
23112343
}
23122344

23132345
func pollIndex(_ req: PollIndexRequest) async throws -> VoidResponse {
@@ -2348,6 +2380,14 @@ fileprivate extension IndexStoreDB {
23482380
}
23492381
return occurrences(ofUSR: usr, roles: [.declaration])
23502382
}
2383+
2384+
/// Find a `SymbolOccurrence` that is considered the primary definition of the symbol with the given USR.
2385+
///
2386+
/// If the USR has an ambiguous definition, the most important role of this function is to deterministically return
2387+
/// the same result every time.
2388+
func primaryDefinitionOrDeclarationOccurrence(ofUSR usr: String) -> SymbolOccurrence? {
2389+
return definitionOrDeclarationOccurrences(ofUSR: usr).sorted().first
2390+
}
23512391
}
23522392

23532393
extension IndexSymbolKind {
@@ -2394,7 +2434,11 @@ extension IndexSymbolKind {
23942434
extension SymbolOccurrence {
23952435
/// Get the name of the symbol that is a parent of this symbol, if one exists
23962436
func getContainerName() -> String? {
2397-
return relations.first(where: { $0.roles.contains(.childOf) })?.symbol.name
2437+
let containers = relations.filter { $0.roles.contains(.childOf) }
2438+
if containers.count > 1 {
2439+
logger.fault("Expected an occurrence to a child of at most one symbol, not multiple")
2440+
}
2441+
return containers.sorted().first?.symbol.name
23982442
}
23992443
}
24002444

0 commit comments

Comments
 (0)