Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 11 additions & 0 deletions Sources/SemanticIndex/CheckedIndex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,17 @@ package final class CheckedIndex {
return try index.symbols(inFilePath: path)
}

/// All symbol occurrences recorded in the index for the given file, filtered to those that are up-to-date.
///
/// Unlike `symbols(inFilePath:)`, each occurrence carries its `SymbolLocation` (line/column), so callers
/// can resolve the symbol(s) at a specific position without opening the document or invoking sourcekitd.
package func symbolOccurrences(inFilePath path: String) throws -> [SymbolOccurrence] {
guard try self.hasAnyUpToDateUnit(for: DocumentURI(filePath: path, isDirectory: false)) else {
return []
}
return try index.symbolOccurrences(inFilePath: path).filter { checker.isUpToDate($0.location) }
}

/// Returns all unit test symbol in unit files that reference one of the main files in `mainFilePaths`.
package func unitTests(referencedByMainFiles mainFilePaths: [String]) throws -> [SymbolOccurrence] {
return try index.unitTests(referencedByMainFiles: mainFilePaths).filter { checker.isUpToDate($0.location) }
Expand Down
155 changes: 88 additions & 67 deletions Sources/SourceKitLSP/SourceKitLSPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
case let request as RequestAndReply<CallHierarchyOutgoingCallsRequest>:
await request.reply { try await outgoingCalls(request.params) }
case let request as RequestAndReply<CallHierarchyPrepareRequest>:
await self.handleRequest(for: request, requestHandler: self.prepareCallHierarchy)
await request.reply { try await prepareCallHierarchy(request.params) }
case let request as RequestAndReply<CodeActionRequest>:
await self.handleRequest(for: request, requestHandler: self.codeAction)
case let request as RequestAndReply<CodeActionResolveRequest>:
Expand Down Expand Up @@ -691,7 +691,7 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
case let request as RequestAndReply<HoverRequest>:
await self.handleRequest(for: request, requestHandler: self.hover)
case let request as RequestAndReply<ImplementationRequest>:
await self.handleRequest(for: request, requestHandler: self.implementation)
await request.reply { try await self.implementation(request.params) }
case let request as RequestAndReply<IndexedRenameRequest>:
await self.handleRequest(for: request, requestHandler: self.indexedRename)
case let request as RequestAndReply<InitializeRequest>:
Expand All @@ -709,7 +709,7 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
case let request as RequestAndReply<PrepareRenameRequest>:
await self.handleRequest(for: request, requestHandler: self.prepareRename)
case let request as RequestAndReply<ReferencesRequest>:
await self.handleRequest(for: request, requestHandler: self.references)
await request.reply { try await self.references(request.params) }
case let request as RequestAndReply<RenameRequest>:
await request.reply { try await rename(request.params) }
case let request as RequestAndReply<SetOptionsRequest>:
Expand All @@ -725,7 +725,7 @@ extension SourceKitLSPServer: QueueBasedMessageHandler {
case let request as RequestAndReply<TriggerReindexRequest>:
await request.reply { try await triggerReindex(request.params) }
case let request as RequestAndReply<TypeHierarchyPrepareRequest>:
await self.handleRequest(for: request, requestHandler: self.prepareTypeHierarchy)
await request.reply { try await self.prepareTypeHierarchy(request.params) }
case let request as RequestAndReply<TypeHierarchySubtypesRequest>:
await request.reply { try await subtypes(request.params) }
case let request as RequestAndReply<TypeHierarchySupertypesRequest>:
Expand Down Expand Up @@ -2213,22 +2213,58 @@ extension SourceKitLSPServer {
return .locations(remappedLocations)
}

func implementation(
_ req: ImplementationRequest,
/// Resolve the USR(s) of the symbol at `position` in `uri`.
///
/// - If the editor has the document open, use cursor info from the live buffer. This is the most
/// precise option and also resolves local, non-indexed symbols.
/// - Otherwise, resolve the symbol from `index` by finding the occurrence recorded at the requested
/// position, mapping its location to an LSP position using the file's on-disk contents. This avoids
/// opening the document (no language service, no sourcekitd cursor info) and lets these requests
/// work for symbols whose definition lives in a file the editor hasn't opened.
///
/// The returned USRs are sorted and de-duplicated so that results are deterministic.
private func usrsOfSymbol(
at position: Position,
in uri: DocumentURI,
workspace: Workspace,
languageService: any LanguageService
) async throws -> LocationsOrLocationLinksResponse? {
let symbols = try await languageService.symbolInfo(
SymbolInfoRequest(
textDocument: req.textDocument,
position: req.position
index: CheckedIndex?
) async throws -> [String] {
var usrs: [String]
if let openLanguageService = workspace.languageServices(forOpenDocument: uri).first {
let symbols = try await openLanguageService.symbolInfo(
SymbolInfoRequest(textDocument: TextDocumentIdentifier(uri), position: position)
)
)
guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else {
usrs = symbols.compactMap(\.usr)
} else {
guard let index, let filePath = uri.fileURL?.path else {
return []
}
let language = Language(inferredFromFileExtension: uri) ?? .swift
guard let snapshot = documentManager.latestSnapshotOrDisk(uri, language: language) else {
return []
}
usrs = try index.symbolOccurrences(inFilePath: filePath)
.filter { snapshot.position(of: $0.location) == position }
.map { $0.symbol.usr }
}
usrs.sortAndDedupe()
return usrs
}

func implementation(
_ req: ImplementationRequest
) async throws -> LocationsOrLocationLinksResponse? {
let uri = req.textDocument.uri
guard let workspace = await self.workspaceForDocument(uri: uri) else {
throw ResponseError.workspaceNotOpen(uri)
}
guard let index = await workspace.index(checkedFor: .deletedFiles) else {
return nil
}
let locations = try symbols.flatMap { (symbol) -> [Location] in
guard let usr = symbol.usr else { return [] }

let usrs = try await self.usrsOfSymbol(at: req.position, in: uri, workspace: workspace, index: index)

let locations = try usrs.flatMap { (usr) -> [Location] in
var occurrences = try index.occurrences(ofUSR: usr, roles: .baseOf)
if occurrences.isEmpty {
occurrences = try index.occurrences(relatedToUSR: usr, roles: .overrideOf)
Expand All @@ -2242,19 +2278,18 @@ extension SourceKitLSPServer {
}

func references(
_ req: ReferencesRequest,
workspace: Workspace,
languageService: any LanguageService
_ req: ReferencesRequest
) async throws -> [Location] {
let symbols = try await languageService.symbolInfo(
SymbolInfoRequest(
textDocument: req.textDocument,
position: req.position
)
)
let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles)
let indexLocations = try symbols.flatMap { symbol -> [Location] in
guard let usr = symbol.usr, let index else { return [] }
let uri = req.textDocument.uri
guard let workspace = await self.workspaceForDocument(uri: uri) else {
throw ResponseError.workspaceNotOpen(uri)
}
let index = await workspace.index(checkedFor: .deletedFiles)

let usrs = try await self.usrsOfSymbol(at: req.position, in: uri, workspace: workspace, index: index)

let indexLocations = try usrs.flatMap { usr -> [Location] in
guard let index else { return [] }
logger.info("Finding references for USR \(usr)")
var roles: SymbolRole = [.reference]
if req.context.includeDeclaration {
Expand All @@ -2265,17 +2300,20 @@ extension SourceKitLSPServer {

var locations = indexLocations

let hasCurrentFileIndexResults = indexLocations.contains { $0.uri == req.textDocument.uri }
let hasCurrentFileIndexResults = indexLocations.contains { $0.uri == uri }

if !hasCurrentFileIndexResults {
// `localReferences` handles symbols that aren't in the index (e.g. local variables). Those are
// always in the current file, which the user must have open to invoke on, so this only applies
// to the open-document path.
if !hasCurrentFileIndexResults, let openLanguageService = workspace.languageServices(forOpenDocument: uri).first {
do {
let localLocations = try await languageService.localReferences(
let localLocations = try await openLanguageService.localReferences(
at: req.position,
in: req.textDocument.uri,
in: uri,
includeDeclaration: req.context.includeDeclaration
)
locations += localLocations
} catch let error as ResponseError {
} catch is ResponseError {
logger.debug("localReferences not supported for this language service")
} catch {
logger.error("Unexpected error computing local references: \(String(describing: error))")
Expand Down Expand Up @@ -2310,21 +2348,17 @@ extension SourceKitLSPServer {
}

func prepareCallHierarchy(
_ req: CallHierarchyPrepareRequest,
workspace: Workspace,
languageService: any LanguageService
_ req: CallHierarchyPrepareRequest
) async throws -> [CallHierarchyItem]? {
let symbols = try await languageService.symbolInfo(
SymbolInfoRequest(
textDocument: req.textDocument,
position: req.position
)
)
guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else {
let uri = req.textDocument.uri
guard let workspace = await self.workspaceForDocument(uri: uri) else {
throw ResponseError.workspaceNotOpen(uri)
}
guard let index = await workspace.index(checkedFor: .deletedFiles) else {
return nil
}
// For call hierarchy preparation we only locate the definition
let usrs = symbols.compactMap(\.usr)

let usrs = try await self.usrsOfSymbol(at: req.position, in: uri, workspace: workspace, index: index)

// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
func indexToLSPCallHierarchyItem2(
Expand Down Expand Up @@ -2515,32 +2549,19 @@ extension SourceKitLSPServer {
}

func prepareTypeHierarchy(
_ req: TypeHierarchyPrepareRequest,
workspace: Workspace,
languageService: any LanguageService
_ req: TypeHierarchyPrepareRequest
) async throws -> [TypeHierarchyItem]? {
let symbols = try await languageService.symbolInfo(
SymbolInfoRequest(
textDocument: req.textDocument,
position: req.position
)
)
guard !symbols.isEmpty else {
return nil
let uri = req.textDocument.uri
guard let workspace = await self.workspaceForDocument(uri: uri) else {
throw ResponseError.workspaceNotOpen(uri)
}
guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else {
guard let index = await workspace.index(checkedFor: .deletedFiles) else {
return nil
}
let usrs = symbols.filter {
// Only include references to type. For example, we don't want to find the type hierarchy of a constructor when
// starting the type hierarchy on `Foo()`.
// Consider a symbol a class if its kind is `nil`, eg. for a symbol returned by clang's SymbolInfo, which
// doesn't support the `kind` field.
switch $0.kind {
case .class, .enum, .interface, .struct, nil: return true
default: return false
}
}.compactMap(\.usr)

// The resolved symbols are filtered to types below based on their kind in the index, so there's no
// need to pre-filter by kind here.
let usrs = try await self.usrsOfSymbol(at: req.position, in: uri, workspace: workspace, index: index)

// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
func indexToLSPTypeHierarchyItem2(
Expand Down
56 changes: 56 additions & 0 deletions Tests/SourceKitLSPTests/CallHierarchyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -938,4 +938,60 @@ final class CallHierarchyTests: SourceKitLSPTestCase {
]
)
}

func testPrepareCallHierarchyForSymbolInDocumentThatIsNotOpen() async throws {
// Neither file is opened. Preparing the call hierarchy on the declaration of `foo` should resolve
// the item from the index instead of requiring a language service for the document, and expanding
// its incoming calls (which is index-only) should find the caller in the other file. `Lib.swift`
// also defines `bar` so that the test verifies the requested position is honored — we must get
// `foo`, not `bar` and not every symbol recorded for the file.
let project = try await SwiftPMTestProject(
files: [
"Lib.swift": """
func 1️⃣foo() {}
func 2️⃣bar() {}
""",
"Other.swift": """
func 3️⃣caller() {
4️⃣foo()
5️⃣bar()
}
""",
],
enableBackgroundIndexing: true
)

let prepare = try await project.testClient.send(
CallHierarchyPrepareRequest(
textDocument: TextDocumentIdentifier(try project.uri(for: "Lib.swift")),
position: try project.position(of: "1️⃣", in: "Lib.swift")
)
)
let item = try XCTUnwrap(prepare?.only)
XCTAssertEqual(item.name, "foo()")
XCTAssertEqual(item.uri, try project.uri(for: "Lib.swift"))
XCTAssertEqual(item.selectionRange, Range(try project.position(of: "1️⃣", in: "Lib.swift")))

let calls = try await project.testClient.send(CallHierarchyIncomingCallsRequest(item: item))
XCTAssertEqual(
calls,
[
CallHierarchyIncomingCall(
from: CallHierarchyItem(
name: "caller()",
kind: .function,
tags: nil,
uri: try project.uri(for: "Other.swift"),
range: Range(try project.position(of: "3️⃣", in: "Other.swift")),
selectionRange: Range(try project.position(of: "3️⃣", in: "Other.swift")),
data: [
"usr": "s:9MyLibrary6calleryyF",
"uri": .string(try project.uri(for: "Other.swift").stringValue),
]
),
fromRanges: [Range(try project.position(of: "4️⃣", in: "Other.swift"))]
)
]
)
}
}
32 changes: 32 additions & 0 deletions Tests/SourceKitLSPTests/ImplementationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,36 @@ final class ImplementationTests: SourceKitLSPTestCase {
[Location(uri: try project.uri(for: "b.swift"), range: Range(try project.position(of: "2️⃣", in: "b.swift")))]
)
}

func testImplementationForSymbolInDocumentThatIsNotOpen() async throws {
// Neither file is opened. `implementation` invoked on `MyProto` should resolve the symbol at the
// requested position from the index instead of requiring a language service for the document, and
// return the conforming type in the other file. `a.swift` also defines `OtherProto` so that the
// test verifies the requested position is honored — we must get `MyProto`'s implementations, not
// `OtherProto`'s.
let project = try await SwiftPMTestProject(
files: [
"a.swift": """
protocol 1️⃣MyProto {}
protocol 2️⃣OtherProto {}
""",
"b.swift": """
struct MyStruct: 3️⃣MyProto {}
struct OtherStruct: 4️⃣OtherProto {}
""",
],
enableBackgroundIndexing: true
)

let response = try await project.testClient.send(
ImplementationRequest(
textDocument: TextDocumentIdentifier(try project.uri(for: "a.swift")),
position: try project.position(of: "1️⃣", in: "a.swift")
)
)
XCTAssertEqual(
response?.locations,
[Location(uri: try project.uri(for: "b.swift"), range: Range(try project.position(of: "3️⃣", in: "b.swift")))]
)
}
}
41 changes: 41 additions & 0 deletions Tests/SourceKitLSPTests/ReferencesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,45 @@ final class ReferencesTests: SourceKitLSPTestCase {
XCTAssertEqual(Set(response.map(\.uri)), [libURI, otherURI])
XCTAssertEqual(Set(response.map(\.range.lowerBound)), [libPositions["1️⃣"], otherPositions["2️⃣"]])
}

func testReferencesForSymbolInDocumentThatIsNotOpen() async throws {
// Neither `Lib.swift` nor `Other.swift` is opened. `references` invoked on the declaration of
// `foo` should resolve the symbol at the requested position from the index instead of requiring a
// language service for the document, and return `foo`'s references across the project. `Lib.swift`
// also defines `bar` so that the test verifies the requested position is honored — we must get
// `foo`'s references, not `bar`'s and not every symbol recorded for the file.
let project = try await SwiftPMTestProject(
files: [
"Lib.swift": """
func 1️⃣foo() {}
func 2️⃣bar() {}
""",
"Other.swift": """
func test() {
3️⃣foo()
4️⃣bar()
}
""",
],
enableBackgroundIndexing: true
)

let response = try await project.testClient.send(
ReferencesRequest(
textDocument: TextDocumentIdentifier(try project.uri(for: "Lib.swift")),
position: try project.position(of: "1️⃣", in: "Lib.swift"),
context: ReferencesContext(includeDeclaration: true)
)
)
XCTAssertEqual(
response,
[
Location(uri: try project.uri(for: "Lib.swift"), range: Range(try project.position(of: "1️⃣", in: "Lib.swift"))),
Location(
uri: try project.uri(for: "Other.swift"),
range: Range(try project.position(of: "3️⃣", in: "Other.swift"))
),
]
)
}
}
Loading