From dbb1ec7bbe58c8f3c670b026ee2b7af2f4f6e729 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Tue, 28 Apr 2026 20:34:36 -0700 Subject: [PATCH 01/13] Add sourcekit/workspace/symbolNames and sourcekit/workspace/symbolInfo These two new SourceKit-LSP extension requests power "Open Quickly" in editors: the client first calls `sourcekit/workspace/symbolNames` to fetch a sorted, deduplicated list of all symbol names from the index, then calls `sourcekit/workspace/symbolInfo` with a chosen name to retrieve full symbol details (kind, location, container name, USR). For symbols located in `.swiftinterface` / `.swiftmodule` files, the response uses `WorkspaceSymbol` with a deferred location and a `data` payload (containing the USR) so the client can resolve the concrete interface position lazily via the standard `workspaceSymbol/resolve` request. Clients that do not advertise `workspace.symbol.resolveSupport` receive a plain `SymbolInformation` instead. The existing `workspace/symbol` handler is refactored to share the new `workspaceSymbolItem(for:in:copiedFileMap:)` helper. Also adds: - `Array.isSortedAndUnique` / `sortAndDedupe()` (specialised for String) to efficiently deduplicate the symbol name list - `clientSupportsWorkspaceSymbolResolve` capability check - Documentation for Open Quickly and Jump to Definition --- Contributor Documentation/LSP Extensions.md | 59 +++++ Documentation/Jump to Definition.md | 152 +++++++++++ Documentation/Open Quickly.md | 242 ++++++++++++++++++ Sources/SemanticIndex/CheckedIndex.swift | 7 + Sources/SourceKitLSP/CapabilityRegistry.swift | 12 + .../MessageHandlingDependencyTracker.swift | 6 + Sources/SourceKitLSP/SourceKitLSPServer.swift | 224 +++++++++++++--- Sources/SourceKitLSP/Workspace.swift | 2 +- .../SwiftExtensions/Array+SortAndDedupe.swift | 45 ++++ .../SwiftLanguageService/MacroExpansion.swift | 2 +- .../SwiftLanguageService/OpenInterface.swift | 2 +- .../WorkspaceSymbolInfoTests.swift | 209 +++++++++++++++ 12 files changed, 930 insertions(+), 32 deletions(-) create mode 100644 Documentation/Jump to Definition.md create mode 100644 Documentation/Open Quickly.md create mode 100644 Sources/SwiftExtensions/Array+SortAndDedupe.swift create mode 100644 Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift diff --git a/Contributor Documentation/LSP Extensions.md b/Contributor Documentation/LSP Extensions.md index 25e5a2f7a..7e6d3cd13 100644 --- a/Contributor Documentation/LSP Extensions.md +++ b/Contributor Documentation/LSP Extensions.md @@ -389,6 +389,65 @@ export interface IsIndexingResult { } ``` +## `sourcekit/workspace/symbolInfo` + +New request that returns structured location information for a list of exact symbol names. + +Unlike the standard `workspace/symbol` request (which accepts a fuzzy query string), this request takes exact names — typically obtained from `sourcekit/workspace/symbolNames` — and returns all index occurrences for each name across every workspace. + +For each name the response contains zero or more `WorkspaceSymbolItem` values: +- Source-file symbols carry a `SymbolInformation` with a `file://` URI and the exact 0-based line/column. +- SDK/stdlib symbols carry a `WorkspaceSymbol` with `location: .uri(...)` pointing at the `file://` URI of the `.swiftinterface` or `.swiftmodule` file, with the fully-qualified module name as a `?module=` query parameter. The symbol's USR is stored in `data["usr"]`. Call `workspaceSymbol/resolve` to obtain the exact `Location` within the generated interface. The client must advertise `workspace.symbol.resolveSupport`; without it, the raw `file://` URI is returned as `SymbolInformation` instead. + +Every requested name is present in the response as a flat array; items carry their name in the `name` field of the `SymbolInformation` or `WorkspaceSymbol`. + +> [!IMPORTANT] +> This request is experimental and may be modified or removed in future versions of SourceKit-LSP without notice. Do not rely on it. + +- params: `WorkspaceSymbolInfoParams` +- result: `WorkspaceSymbolInfoResult` + +```ts +export interface WorkspaceSymbolInfoParams { + /** + * Symbol names to resolve. + */ + names: string[]; +} + +export interface WorkspaceSymbolInfoResult { + /** + * Flat list of all symbols matching the requested names. + * Each item carries the symbol name in its `name` field. + */ + results: WorkspaceSymbolItem[]; +} +``` + +## `sourcekit/workspace/symbolNames` + +New request that returns the flat, deduplicated list of every symbol name in the workspace index, including names from indexed system modules (stdlib, SDK frameworks). + +Clients use this list to drive a local search UI (fuzzy matching, prefix filtering, etc.) without a round-trip per keystroke. After the user selects a name, send a `sourcekit/workspace/symbolInfo` request to resolve it to concrete locations. + +> [!IMPORTANT] +> This request is experimental and may be modified or removed in future versions of SourceKit-LSP without notice. Do not rely on it. + +- params: `WorkspaceSymbolNamesParams` +- result: `WorkspaceSymbolNamesResult` + +```ts +export interface WorkspaceSymbolNamesParams {} + +export interface WorkspaceSymbolNamesResult { + /** + * Flat, deduplicated list of all symbol names in the workspace index, + * including names from system modules (stdlib, SDK). + */ + names: string[]; +} +``` + ## `window/didChangeActiveDocument` New notification from the client to the server, telling SourceKit-LSP which document is the currently active primary document. diff --git a/Documentation/Jump to Definition.md b/Documentation/Jump to Definition.md new file mode 100644 index 000000000..519f72e03 --- /dev/null +++ b/Documentation/Jump to Definition.md @@ -0,0 +1,152 @@ +# Jump to Definition + +Jump to definition for SDK/stdlib symbols works by generating a +textual Swift interface on demand and returning a `sourcekit-lsp://` +URI that the client can fetch via `workspace/getReferenceDocument`. + +## Requests Involved + +| Request | Direction | Purpose | +|---|---|---| +| `textDocument/definition` | Client → Server | Resolve the symbol under the cursor to a location | +| `workspace/getReferenceDocument` | Client → Server | Fetch the content of a `sourcekit-lsp://` URI | + +`workspace/getReferenceDocument` is a SourceKit-LSP extension. The +client must advertise support in `ClientCapabilities.experimental`: + +```json +{ "workspace/getReferenceDocument": { "supported": true } } +``` + +Without this capability the server writes the interface to a temporary +file and returns a `file://` URI instead. + +## Workflow + +``` +Client Server + │ │ + │── textDocument/definition ──────────────▶│ + │◀─ Location { │ + │ uri: "sourcekit-lsp://...", │ + │ range: { line: 42, character: 14 } │ + │ } │ + │ │ + │── workspace/getReferenceDocument ───────▶│ + │◀─ { content: "..." } ────────────────────│ + │ │ + │ [open tab, scroll to range] │ +``` + +1. **Definition** — the client requests the definition of the symbol + at the cursor. For source-defined symbols the server returns a + `file://` URI with the exact source location. For SDK/stdlib + symbols it returns a `sourcekit-lsp://` URI and sets `range` to + the symbol's position within the generated interface (computed + server-side via `editor.find_usr`). +2. **Content retrieval** — the client fetches the generated interface + via `workspace/getReferenceDocument` to display its content. The + client scrolls to `range` from the definition response — `symbolPosition` + is not used here since the position is already known from step 1. + +## Server-Side Flow + +### 1. `textDocument/definition` handling + +The server first attempts an index-based lookup +(`indexBasedDefinition`). For system/SDK symbols the index record +points to a `.swiftinterface` or `.swiftmodule` file, so the handler +calls: + +``` +definitionInInterface( + moduleName: , + groupName: , + symbolUSR: , + originatorUri: +) +``` + +### 2. `openGeneratedInterface` + +`definitionInInterface` delegates to +`SwiftLanguageService.openGeneratedInterface`, which: + +1. Constructs a fully-resolved `GeneratedInterfaceDocumentURLData` + using `init(moduleName:groupName:primaryFile:)`: + - `sourcekitdDocumentName` is synthesised as + `..` where `hash` is + `abs(buildSettingsFile.stringValue.hashValue)`. + - `buildSettingsFrom` is set to `originatorUri.buildSettingsFile` + — the build settings file of the **requesting source file**, not + the module file. This ensures sourcekitd uses the same compiler + arguments as the file that triggered the request. +2. Calls `generatedInterfaceManager.position(ofUsr:in:)` to find the + symbol's position within the generated interface (see below). +3. Returns `GeneratedInterfaceDetails(uri: sourcekit-lsp://..., + position: )`. + +The URI has no USR fragment. The position is returned separately and +used as `Location.range` in the definition response. + +### 3. Interface generation and caching + +`GeneratedInterfaceManager` opens the interface in sourcekitd via +`editor.open.interface`: + +``` +keys.name: ".." +keys.moduleName: "" +keys.groupName: "" // if present +keys.synthesizedExtension: 1 +keys.compilerArgs: [... compiler arguments from build settings ...] +``` + +The resulting `sourceText` is cached in memory keyed by +`sourcekitdDocumentName`. Subsequent requests for the same module + +build context reuse the cached snapshot. + +### 4. Symbol position within the interface + +`GeneratedInterfaceManager.position(ofUsr:in:)` sends +`editor.find_usr` to sourcekitd: + +``` +keys.sourceFile: "" +keys.usr: "" +``` + +sourcekitd returns a byte offset, which is converted to a 0-based +`Position` via `DocumentSnapshot.positionOf(utf8Offset:)`. + +### 5. URI returned to the client + +The `sourcekit-lsp://` URI is fully resolved — `sourcekitdDocument` +is always present, and there is no USR fragment: + +``` +sourcekit-lsp://generated-swift-interface/Swift.String.swiftinterface + ?moduleName=Swift + &groupName=String + &sourcekitdDocument=Swift.String.12345678 + &buildSettingsFrom=file:///path/to/main.swift +``` + +The `range` in the returned `Location` carries the symbol's position +in the interface, so the client knows where to scroll without calling +`workspace/getReferenceDocument` first. + +### 6. `workspace/getReferenceDocument` handling + +Because the URI is fully resolved (`sourcekitdDocumentName != nil`) +the server skips the stub-resolution path and goes straight to the +language service: + +```swift +primaryLanguageService(for: buildSettingsUri, ...).getReferenceDocument(req) +``` + +`SwiftLanguageService.getReferenceDocument` retrieves the cached +interface snapshot. Since the URI carries no USR fragment, +`symbolPosition` in the response is `nil` — the client uses +`Location.range` from the definition response instead. diff --git a/Documentation/Open Quickly.md b/Documentation/Open Quickly.md new file mode 100644 index 000000000..d782294ea --- /dev/null +++ b/Documentation/Open Quickly.md @@ -0,0 +1,242 @@ +# Open Quickly + +Open Quickly is a feature that lets editors provide fast symbol navigation across the entire workspace, including symbols defined in SDK `.swiftinterface` files. It is built on four LSP extensions that work together in a four-phase flow. + +## LSP Extensions + +### `sourcekit/workspace/symbolNames` — Discovery + +Returns the flat list of every symbol name currently in the workspace index. The client uses this list to drive its search UI (fuzzy matching, prefix filtering, etc.). + +``` +→ WorkspaceSymbolNamesRequest {} +← WorkspaceSymbolNamesResponse { + names: ["String", "Array", "Dictionary", "MyViewController", ...] + } +``` + +### `sourcekit/workspace/symbolInfo` — Resolution + +Given a list of names selected by the client after searching, returns structured location information for each name. Unlike the standard `workspace/symbol` request (which maps a query string to matching symbols), this request takes exact names and returns all occurrences. + +The shape of each result item depends on the symbol's origin: + +**Source-file symbols** — returned as `SymbolInformation` with a `file://` URI and the exact 0-based line/column from the index. + +``` +→ WorkspaceSymbolInfoRequest { names: ["MyViewController"] } +← WorkspaceSymbolInfoResponse { + results: [ + SymbolInformation { + name: "MyViewController", + kind: .class, + location: Location { + uri: "file:///path/to/MyViewController.swift", + range: { line: 3, character: 0 } + } + } + ] + } +``` + +**SDK/stdlib symbols** — returned as `WorkspaceSymbol` with `location: .uri(file:// URL)` (no range) pointing to the `.swiftinterface` or `.swiftmodule` file from the index record, when the client advertises `workspace.symbol.resolveSupport`. The fully-qualified module name (e.g. `Swift.String`) is appended as a `?module=` query parameter on the location URL so clients can derive a display path without inspecting the `data` dictionary. The symbol's USR is stored in the `data` dictionary. The client must call `workspaceSymbol/resolve` to obtain the exact location within the generated interface. + +``` +→ WorkspaceSymbolInfoRequest { names: ["String"] } +← WorkspaceSymbolInfoResponse { + results: [ + WorkspaceSymbol { + name: "String", + kind: .struct, + location: { uri: "file:///path/to/Swift.swiftmodule/arm64-apple-macosx.swiftinterface?module=Swift.String" }, + data: { "usr": "s:SS" } + } + ] + } +``` + +Without that capability, the raw `file://` URI of the `.swiftinterface` or `.swiftmodule` file from the index record is returned as `SymbolInformation` instead. + +The response is a flat array of `WorkspaceSymbolItem` values. Each item carries the symbol name in its `name` field. + +### `workspaceSymbol/resolve` — Location Resolution + +Resolves the lazy location of a `WorkspaceSymbol` returned by `sourcekit/workspace/symbolInfo`. The server parses `moduleName` and `groupName` from the `?module=` query parameter of the location URL, reads `usr` from the `data` dictionary, opens the generated Swift interface for the symbol's module, finds the symbol's position using the USR, and returns the same symbol with `location` replaced by a full `Location` (URI + range). + +``` +→ WorkspaceSymbolResolveRequest { + workspaceSymbol: WorkspaceSymbol { + name: "String", + kind: .struct, + location: { uri: "file:///path/to/Swift.swiftmodule/arm64-apple-macosx.swiftinterface?module=Swift.String" }, + data: { "usr": "s:SS" } + } + } +← WorkspaceSymbol { + name: "String", + kind: .struct, + location: Location { + uri: "sourcekit-lsp://generated-swift-interface/Swift.String.swiftinterface?moduleName=Swift&groupName=String&sourcekitdDocument=Swift.String.12345678&buildSettingsFrom=file:///path/to/Sources/main.swift", + range: { line: 42, character: 14 } + } + } +``` + +The resolved URI is a fully-parameterized `sourcekit-lsp://generated-swift-interface/` URL containing `sourcekitdDocument` and `buildSettingsFrom` derived from a real source file in the workspace via `mainFiles(containing:)`. + +The client must treat the resolved `sourcekit-lsp://` URI as **opaque** — it should not parse or extract information from the query parameters. The path component (e.g. `Swift.String.swiftinterface`) may be used as the editor tab title. The URI is otherwise only valid as an input to `workspace/getReferenceDocument`; its query parameter structure is an implementation detail subject to change. + +### `workspace/getReferenceDocument` — Content Retrieval + +Fetches the text content of a reference document URI (e.g. a generated Swift interface). This is a pure content provider — it returns the document text and nothing else. + +``` +→ GetReferenceDocumentRequest { uri: "sourcekit-lsp://generated-swift-interface/...?sourcekitdDocument=...&..." } +← GetReferenceDocumentResponse { + content: "// Swift.String\n...\npublic struct String { ... }" + } +``` + +The URI passed here must be a fully resolved URI (with `sourcekitdDocument` set), as returned by `workspaceSymbol/resolve`. + +## Workflow + +``` +Client Server + │ │ + │── sourcekit/workspace/symbolNames ──────────────▶│ + │◀─ { ["String", "Array", ...] } ──────────────────│ + │ │ + │ [user types "Str"] │ + │ │ + │── sourcekit/workspace/symbolInfo │ + │ {["String", "Stride", ...]} ────────────────▶│ + │◀─ [WorkspaceSymbol] ─────────────────────────────│ + │ (location: "file://...?module=Swift.String") │ + │ │ + │ [user selects "String"] │ + │ │ + │── workspaceSymbol/resolve ──────────────────────▶│ + │◀─ WorkspaceSymbol │ + │ (url: "sourcekit-lsp://...", range: ...) ────│ + │ │ + │── workspace/getReferenceDocument ───────────────▶│ + │◀─ { content: "..." } ────────────────────────────│ + │ │ + │ [open tab, scroll to range.start] │ +``` + +1. **Discovery** — fetch all names; client filters locally. +2. **Resolution** — send matching name(s) to populate the search result list; server returns symbol details (kind, container name, location) for display. + - Source symbols: `SymbolInformation` with a `file://` URI and exact 0-based line/column. No further steps required. + - SDK/stdlib symbols: `WorkspaceSymbol` with `location: .uri(file:// URL?module=...)` pointing to the module file and the USR in `data["usr"]`, when the client advertises `workspace.symbol.resolveSupport`. Otherwise falls back to `SymbolInformation` with the raw `file://` URI. +3. **Location resolution** — call `workspaceSymbol/resolve` with the selected `WorkspaceSymbol` to open the generated interface and resolve the symbol position. The server synthesizes the final `sourcekit-lsp://` URI and fills in `location.range`. +4. **Content retrieval** — fetch the generated interface text. The editor scrolls to `location.range.start` from the resolve step. + +## Pre-resolve Location Design for SDK/stdlib Symbols + +When `sourcekit/workspace/symbolInfo` returns a `WorkspaceSymbol` for an SDK or stdlib symbol, the location is a `file://` URL pointing to the `.swiftinterface` or `.swiftmodule` file recorded in the index, with the fully-qualified module name appended as a `?module=` query parameter. The `data` field carries only the USR. There is no special URI scheme to parse. + +`DocumentURI` equality and hashing use the filesystem path (via `withUnsafeFileSystemRepresentation`), which strips query parameters, so the URL with `?module=` compares equal to the clean path for all index and build-system lookups. + +### `WorkspaceSymbol` fields + +| Field | Value | +|---|---| +| `location` | `.uri(LocationURI)` — a `file://` URL to the `.swiftinterface` or `.swiftmodule` file, with `?module=` appended (e.g. `?module=Swift.String`) | +| `data["usr"]` | Unified Symbol Reference string (e.g. `"s:SS"`) — used by `workspaceSymbol/resolve` to pinpoint the symbol's line/column within the generated interface | + +### `?module=` query parameter + +The `?module=` value is the fully-qualified dotted module name recorded in the index (e.g. `Swift.String`, `Foundation`). The server appends it when constructing the location URL: + +```swift +var urlComponents = URLComponents(string: moduleFileURI.stringValue)! +urlComponents.queryItems = [URLQueryItem(name: "module", value: fullModuleName)] +``` + +`workspaceSymbol/resolve` splits the value on the first `.` to derive `moduleName` and `groupName` for sourcekitd: + +```swift +// fullModuleName = "Swift.String" +moduleName = "Swift" // passed to openGeneratedInterface +groupName = "String" // passed to openGeneratedInterface +``` + +If there is no dot (e.g. `Foundation`), `groupName` is absent. + +### Example pre-resolve `WorkspaceSymbol` + +**`Swift.String`** (USR `s:SS`) + +``` +WorkspaceSymbol { + name: "String", + kind: .struct, + location: .uri("file:///path/to/Swift.swiftmodule/arm64-apple-macosx.swiftinterface?module=Swift.String"), + data: { "usr": "s:SS" } +} +``` + +### `workspaceSymbol/resolve` transformation + +The resolve step parses the location URL, extracts the clean file path (query excluded via `urlComponents.path`) for `mainFiles(containing:)`, then opens the generated interface via sourcekitd: + +1. Parse `?module=Swift.String` from `uriOnly.uri.arbitrarySchemeURL`; split at first `.` → `moduleName`, `groupName` +2. Read `usr` from `data["usr"]` +3. Look up a real source file via `mainFiles(containing: moduleFileURI)`, sorted by URL string for determinism; pick `.first` +4. Call `openGeneratedInterface(document: primaryFile, moduleName:, groupName:, symbolUSR:)` +5. Return the symbol with `location` replaced by a full `Location` (resolved `sourcekit-lsp://` URI + range) + +## `sourcekit-lsp://` URI for Resolved Locations + +After `workspaceSymbol/resolve`, the location URI is a fully-parameterized `sourcekit-lsp://generated-swift-interface/` URL. + +### URL Structure + +``` +sourcekit-lsp:///? +``` + +| Component | Value for generated interfaces | +|---|---| +| `document-type` | `generated-swift-interface` | +| `display-name` | Human-readable filename shown in the editor tab/breadcrumb (e.g. `Swift.String.swiftinterface`). **Not used when parsing the URI** — all functional data is in the query parameters. | +| `moduleName` | Top-level module name (e.g. `Swift`) | +| `groupName` | Sub-module / group within the module, if any (e.g. `String`) | +| `sourcekitdDocument` | Passed as `keys.name` to sourcekitd's `editor.open.interface` request — the buffer handle by which sourcekitd tracks the open interface. Synthesized by `workspaceSymbol/resolve` as `..` (e.g. `Swift.String.12345678`) to make the buffer name unique per build-settings context. | +| `buildSettingsFrom` | URI of a real source file in the workspace, obtained via `mainFiles(containing:)`. Used to derive build settings for the generated interface. | + +### `display-name` derivation + +| `moduleName` | `groupName` | `display-name` | +|---|---|---| +| `Swift` | `String` | `Swift.String.swiftinterface` | +| `Foundation` | `NSURLSession` | `Foundation.NSURLSession.swiftinterface` | +| `Foundation` | _(none)_ | `Foundation.swiftinterface` | + +If `groupName` contains `/` (possible for nested groups), the slashes are replaced with `.` in the display name. + +### Example resolved URI + +**`Swift.String`** after `workspaceSymbol/resolve`: + +``` +sourcekit-lsp://generated-swift-interface/Swift.String.swiftinterface + ?moduleName=Swift + &groupName=String + &sourcekitdDocument=Swift.String.12345678 + &buildSettingsFrom=file:///path/to/MyProject/Sources/main.swift +``` + +## Notes + +- _User_ binary `.swiftmodule` files compiled without `-index-store-path` are **not** indexed — there is no index store record for them, so their symbols do not appear in `sourcekit/workspace/symbolNames` or `sourcekit/workspace/symbolInfo`. +- _System/non-user_ binary modules (`isNonUserModule() == true`) **are** indexed by the Swift compiler when `indexSystemModules` is enabled (`IndexRecord.cpp: emitDataForSwiftSerializedModule`): + - *Resilient* system modules: the compiler reloads from the adjacent `.swiftinterface` before indexing. If no interface is available, the module is skipped entirely. + - *Non-resilient* system modules and the stdlib: indexed directly from the binary; symbol locations in the index point to the `.swiftmodule` file. +- Both `.swiftinterface` and `.swiftmodule` location paths are handled identically in `workspaceSymbolItem` — both produce a `WorkspaceSymbol` with a `file://` location URI (carrying `?module=`) and a `data` dictionary with the USR, when the client has the required capabilities. sourcekitd can synthesize a textual interface from either form. +- One client capability gates the `WorkspaceSymbol`/`.uri` path in `sourcekit/workspace/symbolInfo`: + - `ClientCapabilities.workspace.symbol.resolveSupport.properties` containing `"location"` or `"location.range"` (LSP 3.17) — signals that the client can call `workspaceSymbol/resolve` to obtain a range-bearing location. + - Without it, `sourcekit/workspace/symbolInfo` returns `SymbolInformation` with the raw `file://` URI from the index record. +- The resolved location URI from `workspaceSymbol/resolve` uses a `sourcekit-lsp://generated-swift-interface/` scheme when the client advertises `GetReferenceDocumentRequest` support, or a temp `file://` path otherwise. Both forms can be used to open the generated interface. diff --git a/Sources/SemanticIndex/CheckedIndex.swift b/Sources/SemanticIndex/CheckedIndex.swift index 61ddf62ce..d0e0fbb93 100644 --- a/Sources/SemanticIndex/CheckedIndex.swift +++ b/Sources/SemanticIndex/CheckedIndex.swift @@ -379,6 +379,13 @@ package final actor UncheckedIndex: Sendable { return CheckedIndex(unchecked: self, checkLevel: checkLevel) } + package nonisolated func allSymbolNames() throws -> [String] { + guard let underlyingIndexStoreDB else { + throw IndexClosedError() + } + return underlyingIndexStoreDB.allSymbolNames() + } + /// Wait for IndexStoreDB to be updated based on new unit files written to disk. package nonisolated func pollForUnitChangesAndWait() { guard let underlyingIndexStoreDB else { diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift index 0a91d9dc3..b888ae450 100644 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ b/Sources/SourceKitLSP/CapabilityRegistry.swift @@ -114,6 +114,18 @@ package final actor CapabilityRegistry { return clientHasExperimentalCapability(WorkspacePlaygroundsRefreshRequest.method) } + package nonisolated var clientHasWorkspaceGetReferenceDocumentSupport: Bool { + return clientHasExperimentalCapability(GetReferenceDocumentRequest.method) + } + + /// Whether the client supports `workspaceSymbol/resolve` for lazy location resolution. + /// Requires `workspace.symbol.resolveSupport.properties` to contain `"location"` or `"location.range"`. + package nonisolated var clientSupportsWorkspaceSymbolResolve: Bool { + return clientCapabilities.workspace?.symbol?.resolveSupport?.properties.contains(where: { + $0 == "location" || $0.hasPrefix("location.") + }) ?? false + } + package nonisolated func clientHasExperimentalCapability(_ name: String) -> Bool { guard case .dictionary(let experimentalCapabilities) = clientCapabilities.experimental else { return false diff --git a/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift b/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift index d6cdbc3a8..1f71a8143 100644 --- a/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift +++ b/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift @@ -244,6 +244,12 @@ package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependenc self = .freestanding case is WorkspaceSymbolResolveRequest: self = .freestanding + case is WorkspaceSymbolNamesRequest: + self = .freestanding + case is WorkspaceSymbolInfoRequest: + self = .freestanding + case is WorkspaceSymbolResolveRequest: + self = .freestanding case is WorkspaceSymbolsRequest: self = .freestanding case is WorkspaceTestsRequest: diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 2f829c867..075a928dd 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -890,6 +890,12 @@ extension SourceKitLSPServer: QueueBasedMessageHandler { await request.reply { try await subtypes(request.params) } case let request as RequestAndReply: await request.reply { try await supertypes(request.params) } + case let request as RequestAndReply: + await request.reply { try await workspaceSymbolNames(request.params) } + case let request as RequestAndReply: + await request.reply { try await workspaceSymbolInfo(request.params) } + case let request as RequestAndReply: + await request.reply { try await workspaceSymbolResolve(request.params) } case let request as RequestAndReply: await request.reply { try await workspaceSymbols(request.params) } case let request as RequestAndReply: @@ -1169,6 +1175,8 @@ extension SourceKitLSPServer { GetReferenceDocumentRequest.method: .dictionary(["version": .int(1)]), DidChangeActiveDocumentNotification.method: .dictionary(["version": .int(1)]), WorkspacePlaygroundsRefreshRequest.method: .dictionary(["version": .int(1)]), + WorkspaceSymbolNamesRequest.method: .dictionary(["version": .int(1)]), + WorkspaceSymbolInfoRequest.method: .dictionary(["version": .int(1)]), ] if let toolchain = await toolchainRegistry.preferredToolchain(containing: [\.swiftc]), toolchain.swiftPlay != nil { experimentalCapabilities[WorkspacePlaygroundsRequest.method] = .dictionary(["version": .int(1)]) @@ -1315,7 +1323,7 @@ extension SourceKitLSPServer { await orLog("Shutting down build server") { await workspace.buildServerManager.shutdown() } - await workspace.index(checkedFor: .deletedFiles)?.unchecked.close() + await workspace.uncheckedIndex?.close() } } } @@ -1761,6 +1769,187 @@ extension SourceKitLSPServer { return try await languageService.signatureHelp(req) } + /// Handle a workspace/symbolNames request, returning the name list. + func workspaceSymbolNames(_ req: WorkspaceSymbolNamesRequest) async throws -> WorkspaceSymbolNamesResponse { + var symbols = try await self.workspaces.asyncFlatMap { workspace in + try await workspace.uncheckedIndex?.allSymbolNames() ?? [] + } + if !symbols.isSortedAndUnique { + symbols.sortAndDedupe() + } + return WorkspaceSymbolNamesResponse(names: symbols) + } + + /// Map a `SymbolOccurrence` from the index to a `WorkspaceSymbolItem` suitable for returning in a + /// `workspace/symbolInfo` response. + private func workspaceSymbolItem( + for symbolOccurrence: SymbolOccurrence, + in index: CheckedIndex, + copiedFileMap: CopiedFileMap + ) throws -> WorkspaceSymbolItem? { + let containerNames = try index.containerNames(of: symbolOccurrence) + let containerName: String? = + if !containerNames.isEmpty { + switch symbolOccurrence.symbol.language { + case .cxx, .c, .objc: containerNames.joined(separator: "::") + case .swift: containerNames.joined(separator: ".") + } + } else { + nil + } + + // For SDK symbols (location in `.swiftinterface`/`.swiftmodule`), return a `WorkspaceSymbol` + // with a deferred location so the client can resolve it via `workspaceSymbol/resolve`. + // Falls through to `SymbolInformation` with regular file:// URL for clients without `workspace.symbol.resolveSupport`. + if capabilityRegistry?.clientSupportsWorkspaceSymbolResolve ?? false, + symbolOccurrence.location.path.hasSuffix(".swiftinterface") + || symbolOccurrence.location.path.hasSuffix(".swiftmodule") + { + // URL: file://.swiftinterface?module= + // Clients use `module` to show e.g. "Swift > String". + guard let documentURL = symbolOccurrence.location.uri?.fileURL else { + return nil + } + guard var urlComponents = URLComponents(url: documentURL, resolvingAgainstBaseURL: false) else { + return nil + } + urlComponents.queryItems = [ + URLQueryItem(name: "module", value: symbolOccurrence.location.moduleName) + ] + guard let locationURL = urlComponents.url else { + return nil + } + + // Data: {"usr": ""} + // Used by `workspaceSymbol/resolve` to find the symbol in the interface. + let usr = symbolOccurrence.symbol.usr + let data: LSPAny? = usr.isEmpty ? nil : .dictionary(["usr": .string(usr)]) + + return WorkspaceSymbolItem.workspaceSymbol( + WorkspaceSymbol( + name: symbolOccurrence.symbol.name, + kind: symbolOccurrence.symbol.kind.asLspSymbolKind(), + containerName: containerName, + location: .uri(.init(uri: DocumentURI(locationURL))), + data: data + ) + ) + } + + guard let symbolLocation = symbolOccurrence.location.lspLocation else { return nil } + let location = symbolLocation.adjusted(for: copiedFileMap) + return WorkspaceSymbolItem.symbolInformation( + SymbolInformation( + name: symbolOccurrence.symbol.name, + kind: symbolOccurrence.symbol.kind.asLspSymbolKind(), + deprecated: nil, + location: location, + containerName: containerName + ) + ) + } + + /// Handle a `workspace/symbolInfo` request. + /// + /// For each name in `req.names`, looks up all canonical occurrences in every workspace index and + /// converts them to `WorkspaceSymbolItem` values: + /// - Source-file symbols get a `file://` URI with the exact 0-based line/column from the index. + /// - SDK/stdlib symbols (index location ends in `.swiftinterface` or `.swiftmodule`) get a + /// `WorkspaceSymbol` with `location: .uri(file:// URL?module=...)` and the USR in `data`, provided + /// the client advertises `workspace.symbol.resolveSupport`. The client should call + /// `workspaceSymbol/resolve` to obtain the exact location within the interface. + /// Without that capability the raw `file://` URI from the index record is returned instead. + /// + /// Every requested name is present as a key in the response, mapping to an empty array when there + /// are no occurrences. + func workspaceSymbolInfo(_ req: WorkspaceSymbolInfoRequest) async throws -> WorkspaceSymbolInfoResponse { + var result: [WorkspaceSymbolItem] = [] + for workspace in workspaces { + guard let index = await workspace.index(checkedFor: .deletedFiles) else { + continue + } + let copiedFileMap = await workspace.buildServerManager.cachedCopiedFileMap + for name in req.names { + var symbols: [SymbolOccurrence] = [] + try index.forEachCanonicalSymbolOccurrence(byName: name) { symbolOccurrence in + symbols.append(symbolOccurrence) + return true + } + try Task.checkCancellation() + for symbol in symbols { + if let item = try self.workspaceSymbolItem(for: symbol, in: index, copiedFileMap: copiedFileMap) { + result.append(item) + } + } + } + } + return WorkspaceSymbolInfoResponse(results: result) + } + + /// Handle a `workspaceSymbol/resolve` request. + /// + /// If the symbol has a `location: .uri(moduleFileURL?module=...)` (as emitted by + /// `workspace/symbolInfo` for SDK/stdlib symbols), opens the generated Swift interface, resolves + /// the symbol position using `data["usr"]`, and returns the symbol with `location: .location(...)`. + /// Symbols with an already-resolved `location: .location(...)` are returned unchanged. + func workspaceSymbolResolve(_ req: WorkspaceSymbolResolveRequest) async throws -> WorkspaceSymbol { + var symbol = req.workspaceSymbol + guard + case .uri(let uriOnly) = symbol.location, + let urlComponents = URLComponents(url: uriOnly.uri.arbitrarySchemeURL, resolvingAgainstBaseURL: false), + let fullModuleName = urlComponents.queryItems?.first(where: { $0.name == "module" })?.value + else { + return symbol + } + + let moduleName: String + let groupName: String? + if let dotIndex = fullModuleName.firstIndex(of: ".") { + moduleName = String(fullModuleName[fullModuleName.startIndex.. [WorkspaceSymbolItem]? { @@ -1795,35 +1984,11 @@ extension SourceKitLSPServer { return true } try Task.checkCancellation() - for symbolOccurrence in symbols.sorted(by: <) { - try Task.checkCancellation() - guard let symbolLocation = symbolOccurrence.location.lspLocation else { continue } - let location = symbolLocation.adjusted(for: copiedFileMap) - - let containerNames = try index.containerNames(of: symbolOccurrence) - let containerName: String? - if containerNames.isEmpty { - containerName = nil - } else { - switch symbolOccurrence.symbol.language { - case .cxx, .c, .objc: containerName = containerNames.joined(separator: "::") - case .swift: containerName = containerNames.joined(separator: ".") - } - } - - items.append( - WorkspaceSymbolItem.symbolInformation( - SymbolInformation( - name: symbolOccurrence.symbol.name, - kind: symbolOccurrence.symbol.kind.asLspSymbolKind(), - deprecated: nil, - location: location, - containerName: containerName - ) - ) - ) + items += try symbols.sorted(by: <).compactMap { + try self.workspaceSymbolItem(for: $0, in: index, copiedFileMap: copiedFileMap) } } + return items } @@ -1979,7 +2144,8 @@ extension SourceKitLSPServer { throw ResponseError.unknown("Unable to infer language for \(buildSettingsUri)") } - return try await primaryLanguageService(for: buildSettingsUri, language, in: workspace).getReferenceDocument(req) + return try await primaryLanguageService(for: buildSettingsUri, language, in: workspace) + .getReferenceDocument(req) } func codeAction( diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 2e6a15061..142e23624 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -176,7 +176,7 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { /// The source code index, if available. /// /// Usually a checked index (retrieved using `index(checkedFor:)`) should be used instead of the unchecked index. - private var uncheckedIndex: UncheckedIndex? { + package var uncheckedIndex: UncheckedIndex? { get async { return await buildServerManager.mainFilesProvider(as: UncheckedIndex.self) } diff --git a/Sources/SwiftExtensions/Array+SortAndDedupe.swift b/Sources/SwiftExtensions/Array+SortAndDedupe.swift new file mode 100644 index 000000000..fbfe32e9c --- /dev/null +++ b/Sources/SwiftExtensions/Array+SortAndDedupe.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 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 +// +//===----------------------------------------------------------------------===// + +extension Array where Element: Comparable { + /// Whether the array's elements are in strictly ascending order with no duplicates. + package var isSortedAndUnique: Bool { + @_specialize(where Element == String) + get { + self.indices.dropFirst().allSatisfy { i in + self[self.index(before: i)] < self[i] + } + } + } + + /// Sorts the array in place and removes duplicate elements. + @_specialize(where Element == String) + package mutating func sortAndDedupe() { + guard self.count > 1 else { + return + } + let remaining = withUnsafeMutableBufferPointer { buf -> Int in + buf.sort() + var writeIdx = buf.startIndex + for readIdx in buf.indices.dropFirst() { + if buf[readIdx] == buf[writeIdx] { + continue + } + buf.formIndex(after: &writeIdx) + buf.swapAt(writeIdx, readIdx) + } + buf.formIndex(after: &writeIdx) + return buf.distance(from: writeIdx, to: buf.endIndex) + } + self.removeLast(remaining) + } +} diff --git a/Sources/SwiftLanguageService/MacroExpansion.swift b/Sources/SwiftLanguageService/MacroExpansion.swift index d7829b94e..02298fd69 100644 --- a/Sources/SwiftLanguageService/MacroExpansion.swift +++ b/Sources/SwiftLanguageService/MacroExpansion.swift @@ -191,7 +191,7 @@ extension SwiftLanguageService { } if self.capabilityRegistry.clientHasExperimentalCapability(PeekDocumentsRequest.method), - self.capabilityRegistry.clientHasExperimentalCapability(GetReferenceDocumentRequest.method) + self.capabilityRegistry.clientHasWorkspaceGetReferenceDocumentSupport { let expansionURIs = try macroExpansionReferenceDocumentURLs.map { try $0.uri } diff --git a/Sources/SwiftLanguageService/OpenInterface.swift b/Sources/SwiftLanguageService/OpenInterface.swift index 52a77e9ed..f24e1a07c 100644 --- a/Sources/SwiftLanguageService/OpenInterface.swift +++ b/Sources/SwiftLanguageService/OpenInterface.swift @@ -42,7 +42,7 @@ extension SwiftLanguageService { nil } - if self.capabilityRegistry.clientHasExperimentalCapability(GetReferenceDocumentRequest.method) { + if self.capabilityRegistry.clientHasWorkspaceGetReferenceDocumentSupport { return GeneratedInterfaceDetails(uri: try urlData.uri, position: position) } let interfaceFilePath = self.generatedInterfacesPath diff --git a/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift b/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift new file mode 100644 index 000000000..da786d4d1 --- /dev/null +++ b/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift @@ -0,0 +1,209 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 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 Foundation +@_spi(SourceKitLSP) import LanguageServerProtocol +import SKTestSupport +import SKUtilities +import SwiftExtensions +import TSCBasic +import ToolchainRegistry +import XCTest + +final class WorkspaceSymbolInfoTests: XCTestCase { + /// Returns the first `WorkspaceSymbol` in a `workspaceSymbolInfo` response for `name` whose + /// location is a `file://` URI to a module file (`.swiftinterface` or `.swiftmodule`). + private func generatedInterfaceSymbol( + for name: String, + in response: WorkspaceSymbolInfoResponse + ) -> WorkspaceSymbol? { + for case .workspaceSymbol(let symbol) in response.results { + if symbol.name == name, + case .uri(let uriOnly) = symbol.location, + let path = uriOnly.uri.fileURL?.path, + path.hasSuffix(".swiftinterface") || path.hasSuffix(".swiftmodule") + { + return symbol + } + } + return nil + } + + func testWorkspaceSymbolNamesContainsSourceSymbols() async throws { + let project = try await IndexedSingleSwiftFileTestProject( + """ + public struct MyStruct {} + public func myFunction() {} + """, + indexSystemModules: true + ) + + let response = try await project.testClient.send(WorkspaceSymbolNamesRequest()) + + assertContains(response.names, "MyStruct") + assertContains(response.names, "myFunction()") + // Stdlib types should be included as it's implicitly imported. + assertContains(response.names, "String") + } + + func testWorkspaceSymbolInfoAndResolveForStdlibSymbol() async throws { + let project = try await IndexedSingleSwiftFileTestProject( + """ + let x: String = "" + """, + capabilities: ClientCapabilities( + workspace: .init(symbol: .init(resolveSupport: .init(properties: ["location"]))), + experimental: [GetReferenceDocumentRequest.method: .dictionary(["supported": .bool(true)])] + ), + indexSystemModules: true + ) + + let response = try await project.testClient.send(WorkspaceSymbolInfoRequest(names: ["String"])) + + // workspace/symbolInfo returns a deferred WorkspaceSymbol for SDK symbols: + // location is a file://?module= URI (no range), USR in data["usr"]. + let symbol = try XCTUnwrap( + generatedInterfaceSymbol(for: "String", in: response), + "Expected a 'String' WorkspaceSymbol with a module file URI location" + ) + guard case .uri(let uriOnly) = symbol.location else { + XCTFail("Expected .uri location, got \(symbol.location)") + return + } + let path = try XCTUnwrap(uriOnly.uri.fileURL?.path) + XCTAssert( + path.hasSuffix(".swiftinterface") || path.hasSuffix(".swiftmodule"), + "Expected a .swiftinterface or .swiftmodule path, got: \(path)" + ) + let urlComponents = try XCTUnwrap(URLComponents(string: uriOnly.uri.arbitrarySchemeURL.absoluteString)) + let queryItems = urlComponents.queryItems + let moduleParam = try XCTUnwrap( + queryItems?.first(where: { $0.name == "module" })?.value, + "URI should contain a ?module= query parameter" + ) + XCTAssertFalse(moduleParam.isEmpty, "?module= query parameter should be non-empty") + XCTAssertNil(urlComponents.fragment, "URI should not contain a fragment") + + guard case .dictionary(let dataDict) = symbol.data, + case .string(let usr) = dataDict["usr"] + else { + XCTFail("Expected data[\"usr\"] string, got \(String(describing: symbol.data))") + return + } + XCTAssertFalse(usr.isEmpty, "Expected non-empty USR in data[\"usr\"]") + + // workspaceSymbol/resolve turns the deferred URI into a sourcekit-lsp:// location with a range. + let resolved = try await project.testClient.send( + WorkspaceSymbolResolveRequest(workspaceSymbol: symbol) + ) + guard case .location(let location) = resolved.location else { + XCTFail("Expected .location after resolve, got \(resolved.location)") + return + } + XCTAssertEqual(location.uri.scheme, "sourcekit-lsp") + + // getReferenceDocument delivers the interface text; the resolved range points at the declaration. + let refDoc = try await project.testClient.send(GetReferenceDocumentRequest(uri: location.uri)) + XCTAssert( + refDoc.content.contains("struct String"), + "Generated interface should contain 'struct String'" + ) + let lineTable = LineTable(refDoc.content) + let line = try XCTUnwrap(lineTable.line(at: location.range.lowerBound.line)) + .trimmingCharacters(in: .whitespaces) + XCTAssert( + line.contains("struct String"), + "Line at resolved position should contain 'struct String', got: '\(line)'" + ) + } + + /// Confirms that symbols from a binary-only `.swiftmodule` (compiled without `-index-store-path`) + /// do not appear in the workspace index, unlike symbols from source-compiled targets. + func testBinarySwiftModuleSymbolsNotIndexed() async throws { + guard let swiftc = await ToolchainRegistry.forTesting.default?.swiftc else { + throw XCTSkip("swiftc not found") + } + + // Compile a Swift module to binary .swiftmodule only — no -index-store-path, + // so its symbols are never written to any index store. + try await withTestScratchDir { binaryModuleDir in + let sourceFile = binaryModuleDir.appendingPathComponent("BinaryLib.swift") + try "public struct BinaryOnlyStruct {}".write(to: sourceFile, atomically: true, encoding: .utf8) + + var args = [ + swiftc.path, + "-emit-module", + "-module-name", "BinaryLib", + "-emit-module-path", binaryModuleDir.appendingPathComponent("BinaryLib.swiftmodule").path, + ] + if let sdk = defaultSDKPath { + args += ["-sdk", sdk] + } + // Pin the deployment target to macOS 10.13 to match SwiftPMTestProject's default, + // so the binary module is importable in the consumer project regardless of SDK version. + #if os(macOS) + #if arch(arm64) + args += ["-target", "arm64-apple-macosx10.13"] + #elseif arch(x86_64) + args += ["-target", "x86_64-apple-macosx10.13"] + #endif + #endif + args += [sourceFile.path] + try await Process.checkNonZeroExit(arguments: args) + + // Create a project that imports BinaryLib via its binary .swiftmodule only. + let project = try await SwiftPMTestProject( + files: [ + "Sources/App/main.swift": """ + import BinaryLib + public struct SourceStruct { + var binary: BinaryOnlyStruct + } + """ + ], + manifest: """ + let package = Package( + name: "App", + targets: [ + .executableTarget( + name: "App", + swiftSettings: [.unsafeFlags(["-I", "\(binaryModuleDir.path)"])] + ) + ] + ) + """, + enableBackgroundIndexing: true, + pollIndex: true + ) + XCTAssert(FileManager.default.fileExists(at: binaryModuleDir.appendingPathComponent("BinaryLib.swiftmodule"))) + + // Confirm the file has no error diagnostics — the binary .swiftmodule is importable. + let (mainUri, _) = try project.openDocument("main.swift") + let diagnostics = try await project.testClient.send( + DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(mainUri)) + ) + let errorDiagnostics = diagnostics.fullReport?.items.filter { $0.severity == .error } ?? [] + XCTAssert(errorDiagnostics.isEmpty, "Expected no errors in main.swift, got: \(errorDiagnostics)") + + let response = try await project.testClient.send(WorkspaceSymbolNamesRequest()) + + // SourceStruct is defined in source and compiled with -index-store-path → it IS indexed. + XCTAssert(response.names.contains("SourceStruct"), "Source-compiled symbol should appear in the index") + + // BinaryOnlyStruct lives only in the .swiftmodule binary → no index record was ever written for it. + XCTAssertFalse( + response.names.contains("BinaryOnlyStruct"), + "Symbol from binary-only .swiftmodule should not appear in the index" + ) + } + } +} From b5a5fb2ed018a480e60fdbf062118ec9ed5fda75 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Wed, 29 Apr 2026 13:58:27 -0700 Subject: [PATCH 02/13] Review --- Contributor Documentation/LSP Extensions.md | 2 +- Documentation/Open Quickly.md | 4 +- Sources/SourceKitLSP/CapabilityRegistry.swift | 2 +- .../MessageHandlingDependencyTracker.swift | 4 +- Sources/SourceKitLSP/SourceKitLSPServer.swift | 81 ++++++++++++++----- Sources/SwiftExtensions/CMakeLists.txt | 1 + .../WorkspaceSymbolInfoTests.swift | 8 -- 7 files changed, 65 insertions(+), 37 deletions(-) diff --git a/Contributor Documentation/LSP Extensions.md b/Contributor Documentation/LSP Extensions.md index 7e6d3cd13..bb50e437c 100644 --- a/Contributor Documentation/LSP Extensions.md +++ b/Contributor Documentation/LSP Extensions.md @@ -396,7 +396,7 @@ New request that returns structured location information for a list of exact sym Unlike the standard `workspace/symbol` request (which accepts a fuzzy query string), this request takes exact names — typically obtained from `sourcekit/workspace/symbolNames` — and returns all index occurrences for each name across every workspace. For each name the response contains zero or more `WorkspaceSymbolItem` values: -- Source-file symbols carry a `SymbolInformation` with a `file://` URI and the exact 0-based line/column. +- Source-file symbols carry a `SymbolInformation` with a `file://` URI and the exact position. - SDK/stdlib symbols carry a `WorkspaceSymbol` with `location: .uri(...)` pointing at the `file://` URI of the `.swiftinterface` or `.swiftmodule` file, with the fully-qualified module name as a `?module=` query parameter. The symbol's USR is stored in `data["usr"]`. Call `workspaceSymbol/resolve` to obtain the exact `Location` within the generated interface. The client must advertise `workspace.symbol.resolveSupport`; without it, the raw `file://` URI is returned as `SymbolInformation` instead. Every requested name is present in the response as a flat array; items carry their name in the `name` field of the `SymbolInformation` or `WorkspaceSymbol`. diff --git a/Documentation/Open Quickly.md b/Documentation/Open Quickly.md index d782294ea..fc4731046 100644 --- a/Documentation/Open Quickly.md +++ b/Documentation/Open Quickly.md @@ -21,7 +21,7 @@ Given a list of names selected by the client after searching, returns structured The shape of each result item depends on the symbol's origin: -**Source-file symbols** — returned as `SymbolInformation` with a `file://` URI and the exact 0-based line/column from the index. +**Source-file symbols** — returned as `SymbolInformation` with a `file://` URI and the range from the index. ``` → WorkspaceSymbolInfoRequest { names: ["MyViewController"] } @@ -128,7 +128,7 @@ Client Server 1. **Discovery** — fetch all names; client filters locally. 2. **Resolution** — send matching name(s) to populate the search result list; server returns symbol details (kind, container name, location) for display. - - Source symbols: `SymbolInformation` with a `file://` URI and exact 0-based line/column. No further steps required. + - Source symbols: `SymbolInformation` with a `file://` URI and exact position. No further steps required. - SDK/stdlib symbols: `WorkspaceSymbol` with `location: .uri(file:// URL?module=...)` pointing to the module file and the USR in `data["usr"]`, when the client advertises `workspace.symbol.resolveSupport`. Otherwise falls back to `SymbolInformation` with the raw `file://` URI. 3. **Location resolution** — call `workspaceSymbol/resolve` with the selected `WorkspaceSymbol` to open the generated interface and resolve the symbol position. The server synthesizes the final `sourcekit-lsp://` URI and fills in `location.range`. 4. **Content retrieval** — fetch the generated interface text. The editor scrolls to `location.range.start` from the resolve step. diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift index b888ae450..1a00244f0 100644 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ b/Sources/SourceKitLSP/CapabilityRegistry.swift @@ -119,7 +119,7 @@ package final actor CapabilityRegistry { } /// Whether the client supports `workspaceSymbol/resolve` for lazy location resolution. - /// Requires `workspace.symbol.resolveSupport.properties` to contain `"location"` or `"location.range"`. + /// Requires `workspace.symbol.resolveSupport.properties` to contain `"location"` or `"location."`. package nonisolated var clientSupportsWorkspaceSymbolResolve: Bool { return clientCapabilities.workspace?.symbol?.resolveSupport?.properties.contains(where: { $0 == "location" || $0.hasPrefix("location.") diff --git a/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift b/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift index 1f71a8143..0c75ce5c8 100644 --- a/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift +++ b/Sources/SourceKitLSP/MessageHandlingDependencyTracker.swift @@ -242,12 +242,10 @@ package enum MessageHandlingDependencyTracker: QueueBasedMessageHandlerDependenc self = .freestanding case is WorkspaceSemanticTokensRefreshRequest: self = .freestanding - case is WorkspaceSymbolResolveRequest: + case is WorkspaceSymbolInfoRequest: self = .freestanding case is WorkspaceSymbolNamesRequest: self = .freestanding - case is WorkspaceSymbolInfoRequest: - self = .freestanding case is WorkspaceSymbolResolveRequest: self = .freestanding case is WorkspaceSymbolsRequest: diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 075a928dd..83df825b9 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -1771,9 +1771,13 @@ extension SourceKitLSPServer { /// Handle a workspace/symbolNames request, returning the name list. func workspaceSymbolNames(_ req: WorkspaceSymbolNamesRequest) async throws -> WorkspaceSymbolNamesResponse { - var symbols = try await self.workspaces.asyncFlatMap { workspace in - try await workspace.uncheckedIndex?.allSymbolNames() ?? [] - } + var symbols = await self.workspaces + .concurrentMap { workspace in + await orLog("Getting symbol names in workspace") { + try await workspace.uncheckedIndex?.allSymbolNames() ?? [] + } ?? [] + } + .flatMap { $0 } if !symbols.isSortedAndUnique { symbols.sortAndDedupe() } @@ -1782,10 +1786,11 @@ extension SourceKitLSPServer { /// Map a `SymbolOccurrence` from the index to a `WorkspaceSymbolItem` suitable for returning in a /// `workspace/symbolInfo` response. - private func workspaceSymbolItem( + private nonisolated func workspaceSymbolItem( for symbolOccurrence: SymbolOccurrence, in index: CheckedIndex, - copiedFileMap: CopiedFileMap + copiedFileMap: CopiedFileMap, + canUseWorkspaceSymbolResolve: Bool ) throws -> WorkspaceSymbolItem? { let containerNames = try index.containerNames(of: symbolOccurrence) let containerName: String? = @@ -1801,7 +1806,7 @@ extension SourceKitLSPServer { // For SDK symbols (location in `.swiftinterface`/`.swiftmodule`), return a `WorkspaceSymbol` // with a deferred location so the client can resolve it via `workspaceSymbol/resolve`. // Falls through to `SymbolInformation` with regular file:// URL for clients without `workspace.symbol.resolveSupport`. - if capabilityRegistry?.clientSupportsWorkspaceSymbolResolve ?? false, + if canUseWorkspaceSymbolResolve, symbolOccurrence.location.path.hasSuffix(".swiftinterface") || symbolOccurrence.location.path.hasSuffix(".swiftmodule") { @@ -1863,25 +1868,46 @@ extension SourceKitLSPServer { /// Every requested name is present as a key in the response, mapping to an empty array when there /// are no occurrences. func workspaceSymbolInfo(_ req: WorkspaceSymbolInfoRequest) async throws -> WorkspaceSymbolInfoResponse { - var result: [WorkspaceSymbolItem] = [] - for workspace in workspaces { + let canUseWorkspaceSymbolResolve = self.capabilityRegistry?.clientSupportsWorkspaceSymbolResolve ?? false + + var groupedResultPerWorkspace = await workspaces.concurrentMap { workspace -> [String: [WorkspaceSymbolItem]] in guard let index = await workspace.index(checkedFor: .deletedFiles) else { - continue + return [:] } + var result: [String: [WorkspaceSymbolItem]] = [:] let copiedFileMap = await workspace.buildServerManager.cachedCopiedFileMap for name in req.names { + if Task.isCancelled { return [:] } var symbols: [SymbolOccurrence] = [] - try index.forEachCanonicalSymbolOccurrence(byName: name) { symbolOccurrence in - symbols.append(symbolOccurrence) - return true + _ = orLog("getting symbol information") { + try index.forEachCanonicalSymbolOccurrence(byName: name) { symbolOccurrence in + symbols.append(symbolOccurrence) + return true + } } - try Task.checkCancellation() - for symbol in symbols { - if let item = try self.workspaceSymbolItem(for: symbol, in: index, copiedFileMap: copiedFileMap) { - result.append(item) + if Task.isCancelled { return [:] } + result[name] = symbols.compactMap { symbol in + orLog("getting symbol information") { + try self.workspaceSymbolItem( + for: symbol, + in: index, + copiedFileMap: copiedFileMap, + canUseWorkspaceSymbolResolve: canUseWorkspaceSymbolResolve + ) } } } + return result + } + + // Flatten the result. + var result: [WorkspaceSymbolItem] = [] + for name in req.names { + for grouped in groupedResultPerWorkspace { + if let items = grouped[name] { + result.append(contentsOf: items) + } + } } return WorkspaceSymbolInfoResponse(results: result) } @@ -1897,7 +1923,7 @@ extension SourceKitLSPServer { guard case .uri(let uriOnly) = symbol.location, let urlComponents = URLComponents(url: uriOnly.uri.arbitrarySchemeURL, resolvingAgainstBaseURL: false), - let fullModuleName = urlComponents.queryItems?.first(where: { $0.name == "module" })?.value + let fullModuleName = urlComponents.queryItems?.last(where: { $0.name == "module" })?.value else { return symbol } @@ -1919,7 +1945,14 @@ extension SourceKitLSPServer { nil } - let moduleFileURI = DocumentURI(filePath: urlComponents.path, isDirectory: false) + let moduleFileURI = DocumentURI( + { + var components = urlComponents + components.fragment = nil + components.query = nil + return components.url! + }() + ) for workspace in workspaces { let mainFile = await workspace.buildServerManager .mainFiles(containing: moduleFileURI) @@ -1960,6 +1993,7 @@ extension SourceKitLSPServer { guard req.query.count >= minWorkspaceSymbolPatternLength else { return [] } + let canUseWorkspaceSymbolResolve = self.capabilityRegistry?.clientSupportsWorkspaceSymbolResolve ?? false var items: [WorkspaceSymbolItem] = [] for workspace in workspaces { guard let index = await workspace.index(checkedFor: .deletedFiles) else { @@ -1985,10 +2019,14 @@ extension SourceKitLSPServer { } try Task.checkCancellation() items += try symbols.sorted(by: <).compactMap { - try self.workspaceSymbolItem(for: $0, in: index, copiedFileMap: copiedFileMap) + try self.workspaceSymbolItem( + for: $0, + in: index, + copiedFileMap: copiedFileMap, + canUseWorkspaceSymbolResolve: canUseWorkspaceSymbolResolve + ) } } - return items } @@ -2144,8 +2182,7 @@ extension SourceKitLSPServer { throw ResponseError.unknown("Unable to infer language for \(buildSettingsUri)") } - return try await primaryLanguageService(for: buildSettingsUri, language, in: workspace) - .getReferenceDocument(req) + return try await primaryLanguageService(for: buildSettingsUri, language, in: workspace).getReferenceDocument(req) } func codeAction( diff --git a/Sources/SwiftExtensions/CMakeLists.txt b/Sources/SwiftExtensions/CMakeLists.txt index 0165ffe02..893f257f1 100644 --- a/Sources/SwiftExtensions/CMakeLists.txt +++ b/Sources/SwiftExtensions/CMakeLists.txt @@ -1,5 +1,6 @@ set(sources Array+Safe.swift + Array+SortAndDedupe.swift AsyncUtils.swift Cache.swift CartesianProduct.swift diff --git a/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift b/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift index da786d4d1..7e239bce4 100644 --- a/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift @@ -93,14 +93,6 @@ final class WorkspaceSymbolInfoTests: XCTestCase { XCTAssertFalse(moduleParam.isEmpty, "?module= query parameter should be non-empty") XCTAssertNil(urlComponents.fragment, "URI should not contain a fragment") - guard case .dictionary(let dataDict) = symbol.data, - case .string(let usr) = dataDict["usr"] - else { - XCTFail("Expected data[\"usr\"] string, got \(String(describing: symbol.data))") - return - } - XCTAssertFalse(usr.isEmpty, "Expected non-empty USR in data[\"usr\"]") - // workspaceSymbol/resolve turns the deferred URI into a sourcekit-lsp:// location with a range. let resolved = try await project.testClient.send( WorkspaceSymbolResolveRequest(workspaceSymbol: symbol) From 29251d16fedaaa9ad18512f4bcfd7a50b78a9368 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Thu, 7 May 2026 11:41:54 -0700 Subject: [PATCH 03/13] JtD revise --- Documentation/Jump to Definition.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Documentation/Jump to Definition.md b/Documentation/Jump to Definition.md index 519f72e03..83781479b 100644 --- a/Documentation/Jump to Definition.md +++ b/Documentation/Jump to Definition.md @@ -4,12 +4,17 @@ Jump to definition for SDK/stdlib symbols works by generating a textual Swift interface on demand and returning a `sourcekit-lsp://` URI that the client can fetch via `workspace/getReferenceDocument`. +`sourcekit-lsp://` URIs are opaque. Clients must treat them as +identifiers and must not parse their structure to extract information. + ## Requests Involved | Request | Direction | Purpose | |---|---|---| | `textDocument/definition` | Client → Server | Resolve the symbol under the cursor to a location | | `workspace/getReferenceDocument` | Client → Server | Fetch the content of a `sourcekit-lsp://` URI | +| `textDocument/didOpen` | Client → Server | Notify the server that the generated interface is open | +| `textDocument/didClose` | Client → Server | Notify the server that the generated interface was closed | `workspace/getReferenceDocument` is a SourceKit-LSP extension. The client must advertise support in `ClientCapabilities.experimental`: @@ -36,6 +41,14 @@ Client Server │◀─ { content: "..." } ────────────────────│ │ │ │ [open tab, scroll to range] │ + │ │ + │── textDocument/didOpen ─────────────────▶│ [increment ref count] + │ │ + │ [user closes the tab] │ + │ │ + │── textDocument/didClose ────────────────▶│ [decrement ref count; + │ │ close in sourcekitd + │ │ when it reaches 0] ``` 1. **Definition** — the client requests the definition of the symbol @@ -48,6 +61,14 @@ Client Server via `workspace/getReferenceDocument` to display its content. The client scrolls to `range` from the definition response — `symbolPosition` is not used here since the position is already known from step 1. +3. **Open notification** — once the client opens the tab it sends + `textDocument/didOpen`, which increments the ref count in + `GeneratedInterfaceManager`. This keeps the interface open in + sourcekitd as long as the tab is open. +4. **Close notification** — when the client closes the tab it sends + `textDocument/didClose`, which decrements the ref count. When the + ref count reaches zero the interface is eligible for eviction from + the cache and will eventually be closed in sourcekitd. ## Server-Side Flow @@ -138,8 +159,7 @@ in the interface, so the client knows where to scroll without calling ### 6. `workspace/getReferenceDocument` handling -Because the URI is fully resolved (`sourcekitdDocumentName != nil`) -the server skips the stub-resolution path and goes straight to the +The server extracts `buildSettingsFrom` from the URI to determine the language service: ```swift From d208786cffcfa2241d16d31a39de78a3893672a0 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Thu, 7 May 2026 11:55:35 -0700 Subject: [PATCH 04/13] clarify --- Documentation/Jump to Definition.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Documentation/Jump to Definition.md b/Documentation/Jump to Definition.md index 83781479b..82fec5a51 100644 --- a/Documentation/Jump to Definition.md +++ b/Documentation/Jump to Definition.md @@ -42,7 +42,12 @@ Client Server │ │ │ [open tab, scroll to range] │ │ │ - │── textDocument/didOpen ─────────────────▶│ [increment ref count] + │── textDocument/didOpen { │ + │ uri: "sourcekit-lsp://...", │ + │ languageId: "swift", │ + │ version: 1, │ + │ text: "" │ [increment ref count] + │ } ────────────────────────────────────▶│ │ │ │ [user closes the tab] │ │ │ @@ -62,8 +67,10 @@ Client Server client scrolls to `range` from the definition response — `symbolPosition` is not used here since the position is already known from step 1. 3. **Open notification** — once the client opens the tab it sends - `textDocument/didOpen`, which increments the ref count in - `GeneratedInterfaceManager`. This keeps the interface open in + `textDocument/didOpen` with the interface content (already fetched + in step 2) as `text`, `languageId` set to `"swift"`, and `version` + set to `1`. The server increments the ref count in + `GeneratedInterfaceManager`, keeping the interface open in sourcekitd as long as the tab is open. 4. **Close notification** — when the client closes the tab it sends `textDocument/didClose`, which decrements the ref count. When the From 496c1cf597ad5a4e66efdb7680408e1b452a3bcd Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Thu, 7 May 2026 13:45:20 -0700 Subject: [PATCH 05/13] no symbolPosition --- Documentation/Jump to Definition.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Documentation/Jump to Definition.md b/Documentation/Jump to Definition.md index 82fec5a51..3356bf297 100644 --- a/Documentation/Jump to Definition.md +++ b/Documentation/Jump to Definition.md @@ -64,8 +64,7 @@ Client Server server-side via `editor.find_usr`). 2. **Content retrieval** — the client fetches the generated interface via `workspace/getReferenceDocument` to display its content. The - client scrolls to `range` from the definition response — `symbolPosition` - is not used here since the position is already known from step 1. + client scrolls to `range` from the definition response. 3. **Open notification** — once the client opens the tab it sends `textDocument/didOpen` with the interface content (already fetched in step 2) as `text`, `languageId` set to `"swift"`, and `version` @@ -174,6 +173,4 @@ primaryLanguageService(for: buildSettingsUri, ...).getReferenceDocument(req) ``` `SwiftLanguageService.getReferenceDocument` retrieves the cached -interface snapshot. Since the URI carries no USR fragment, -`symbolPosition` in the response is `nil` — the client uses -`Location.range` from the definition response instead. +interface snapshot. From 9769c2a5dfb3777462108e3554f0b6152c38e35e Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Thu, 7 May 2026 17:50:25 -0700 Subject: [PATCH 06/13] explain resolve support --- Sources/SourceKitLSP/CapabilityRegistry.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift index 1a00244f0..4c9cdd69d 100644 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ b/Sources/SourceKitLSP/CapabilityRegistry.swift @@ -119,7 +119,12 @@ package final actor CapabilityRegistry { } /// Whether the client supports `workspaceSymbol/resolve` for lazy location resolution. - /// Requires `workspace.symbol.resolveSupport.properties` to contain `"location"` or `"location."`. + /// Requires `workspace.symbol.resolveSupport.properties` to contain `"location"` or a + /// `"location."` prefix (e.g. `"location.range"`). + /// + /// Strictly speaking, replacing the URI requires `"location"` or `"location.uri"`, but + /// VS Code only advertises `"location.range"` while still handling URI changes in practice, + /// so we accept any `"location."` prefix. package nonisolated var clientSupportsWorkspaceSymbolResolve: Bool { return clientCapabilities.workspace?.symbol?.resolveSupport?.properties.contains(where: { $0 == "location" || $0.hasPrefix("location.") From f9f58300e0209146fe86bdfac6d6c2d8549c3955 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Sun, 10 May 2026 11:58:44 -0700 Subject: [PATCH 07/13] WorkspaceSymbolData --- Sources/SourceKitLSP/MessageMetadata.swift | 9 +++++++++ Sources/SourceKitLSP/SourceKitLSPServer.swift | 11 ++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/SourceKitLSP/MessageMetadata.swift b/Sources/SourceKitLSP/MessageMetadata.swift index 7a3898d29..02c76f91f 100644 --- a/Sources/SourceKitLSP/MessageMetadata.swift +++ b/Sources/SourceKitLSP/MessageMetadata.swift @@ -33,3 +33,12 @@ struct ResolveItemData: Codable, Hashable, LSPAnyCodable { self.uri = uri } } + +/// Metadata stored in `WorkspaceSymbol.data` to support `workspaceSymbol/resolve` for SDK symbols. +struct WorkspaceSymbolData: Codable, Hashable, LSPAnyCodable { + var usr: String + + init(usr: String) { + self.usr = usr + } +} diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 83df825b9..2d18ad978 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -1825,10 +1825,8 @@ extension SourceKitLSPServer { return nil } - // Data: {"usr": ""} - // Used by `workspaceSymbol/resolve` to find the symbol in the interface. let usr = symbolOccurrence.symbol.usr - let data: LSPAny? = usr.isEmpty ? nil : .dictionary(["usr": .string(usr)]) + let data: LSPAny? = usr.isEmpty ? nil : WorkspaceSymbolData(usr: usr).encodeToLSPAny() return WorkspaceSymbolItem.workspaceSymbol( WorkspaceSymbol( @@ -1938,12 +1936,7 @@ extension SourceKitLSPServer { groupName = nil } - let usr: String? = - if case .dictionary(let dataDict) = symbol.data, case .string(let usr) = dataDict["usr"] { - usr - } else { - nil - } + let usr = WorkspaceSymbolData(fromLSPAny: symbol.data)?.usr let moduleFileURI = DocumentURI( { From f3a677d724daf670b179f788db70ead1b983f901 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Mon, 11 May 2026 16:55:42 -0700 Subject: [PATCH 08/13] Tighten clientSupportsWorkspaceSymbolResolve to require "location.uri" and "location.range" Previously accepted any "location." prefix to accommodate VS Code, which only advertises "location.range". Now require "location" or both "location.uri" and "location.range". --- Documentation/Open Quickly.md | 6 +++--- Sources/SourceKitLSP/CapabilityRegistry.swift | 16 +++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Documentation/Open Quickly.md b/Documentation/Open Quickly.md index fc4731046..05583e50c 100644 --- a/Documentation/Open Quickly.md +++ b/Documentation/Open Quickly.md @@ -39,7 +39,7 @@ The shape of each result item depends on the symbol's origin: } ``` -**SDK/stdlib symbols** — returned as `WorkspaceSymbol` with `location: .uri(file:// URL)` (no range) pointing to the `.swiftinterface` or `.swiftmodule` file from the index record, when the client advertises `workspace.symbol.resolveSupport`. The fully-qualified module name (e.g. `Swift.String`) is appended as a `?module=` query parameter on the location URL so clients can derive a display path without inspecting the `data` dictionary. The symbol's USR is stored in the `data` dictionary. The client must call `workspaceSymbol/resolve` to obtain the exact location within the generated interface. +**SDK/stdlib symbols** — returned as `WorkspaceSymbol` with `location: .uri(file:// URL)` (no range) pointing to the `.swiftinterface` or `.swiftmodule` file from the index record, when the client advertises `workspace.symbol.resolveSupport.properties` containing `"location"` or both `"location.uri"` and `"location.range"`. The fully-qualified module name (e.g. `Swift.String`) is appended as a `?module=` query parameter on the location URL so clients can derive a display path without inspecting the `data` dictionary. The symbol's USR is stored in the `data` dictionary. The client must call `workspaceSymbol/resolve` to obtain the exact location within the generated interface. ``` → WorkspaceSymbolInfoRequest { names: ["String"] } @@ -129,7 +129,7 @@ Client Server 1. **Discovery** — fetch all names; client filters locally. 2. **Resolution** — send matching name(s) to populate the search result list; server returns symbol details (kind, container name, location) for display. - Source symbols: `SymbolInformation` with a `file://` URI and exact position. No further steps required. - - SDK/stdlib symbols: `WorkspaceSymbol` with `location: .uri(file:// URL?module=...)` pointing to the module file and the USR in `data["usr"]`, when the client advertises `workspace.symbol.resolveSupport`. Otherwise falls back to `SymbolInformation` with the raw `file://` URI. + - SDK/stdlib symbols: `WorkspaceSymbol` with `location: .uri(file:// URL?module=...)` pointing to the module file and the USR in `data["usr"]`, when the client advertises `workspace.symbol.resolveSupport.properties` containing `"location"` or both `"location.uri"` and `"location.range"`. Otherwise falls back to `SymbolInformation` with the raw `file://` URI. 3. **Location resolution** — call `workspaceSymbol/resolve` with the selected `WorkspaceSymbol` to open the generated interface and resolve the symbol position. The server synthesizes the final `sourcekit-lsp://` URI and fills in `location.range`. 4. **Content retrieval** — fetch the generated interface text. The editor scrolls to `location.range.start` from the resolve step. @@ -237,6 +237,6 @@ sourcekit-lsp://generated-swift-interface/Swift.String.swiftinterface - *Non-resilient* system modules and the stdlib: indexed directly from the binary; symbol locations in the index point to the `.swiftmodule` file. - Both `.swiftinterface` and `.swiftmodule` location paths are handled identically in `workspaceSymbolItem` — both produce a `WorkspaceSymbol` with a `file://` location URI (carrying `?module=`) and a `data` dictionary with the USR, when the client has the required capabilities. sourcekitd can synthesize a textual interface from either form. - One client capability gates the `WorkspaceSymbol`/`.uri` path in `sourcekit/workspace/symbolInfo`: - - `ClientCapabilities.workspace.symbol.resolveSupport.properties` containing `"location"` or `"location.range"` (LSP 3.17) — signals that the client can call `workspaceSymbol/resolve` to obtain a range-bearing location. + - `ClientCapabilities.workspace.symbol.resolveSupport.properties` containing `"location"` or both `"location.uri"` and `"location.range"` (LSP 3.17) — signals that the client can call `workspaceSymbol/resolve` to obtain a range-bearing location. - Without it, `sourcekit/workspace/symbolInfo` returns `SymbolInformation` with the raw `file://` URI from the index record. - The resolved location URI from `workspaceSymbol/resolve` uses a `sourcekit-lsp://generated-swift-interface/` scheme when the client advertises `GetReferenceDocumentRequest` support, or a temp `file://` path otherwise. Both forms can be used to open the generated interface. diff --git a/Sources/SourceKitLSP/CapabilityRegistry.swift b/Sources/SourceKitLSP/CapabilityRegistry.swift index 4c9cdd69d..49e2fca6e 100644 --- a/Sources/SourceKitLSP/CapabilityRegistry.swift +++ b/Sources/SourceKitLSP/CapabilityRegistry.swift @@ -119,16 +119,14 @@ package final actor CapabilityRegistry { } /// Whether the client supports `workspaceSymbol/resolve` for lazy location resolution. - /// Requires `workspace.symbol.resolveSupport.properties` to contain `"location"` or a - /// `"location."` prefix (e.g. `"location.range"`). - /// - /// Strictly speaking, replacing the URI requires `"location"` or `"location.uri"`, but - /// VS Code only advertises `"location.range"` while still handling URI changes in practice, - /// so we accept any `"location."` prefix. + /// Requires `workspace.symbol.resolveSupport.properties` to contain `"location"` or + /// both `"location.uri"` and `"location.range"`. package nonisolated var clientSupportsWorkspaceSymbolResolve: Bool { - return clientCapabilities.workspace?.symbol?.resolveSupport?.properties.contains(where: { - $0 == "location" || $0.hasPrefix("location.") - }) ?? false + guard let properties = clientCapabilities.workspace?.symbol?.resolveSupport?.properties else { + return false + } + return properties.contains("location") + || (properties.contains("location.uri") && properties.contains("location.range")) } package nonisolated func clientHasExperimentalCapability(_ name: String) -> Bool { From 3dca0246b6c3176b2662389aae6ed0111c7dc9b4 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Mon, 11 May 2026 16:59:18 -0700 Subject: [PATCH 09/13] Document file://...?module= as a general-purpose URL format in Open Quickly.md --- Documentation/Open Quickly.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Documentation/Open Quickly.md b/Documentation/Open Quickly.md index 05583e50c..16ef49a15 100644 --- a/Documentation/Open Quickly.md +++ b/Documentation/Open Quickly.md @@ -137,6 +137,9 @@ Client Server When `sourcekit/workspace/symbolInfo` returns a `WorkspaceSymbol` for an SDK or stdlib symbol, the location is a `file://` URL pointing to the `.swiftinterface` or `.swiftmodule` file recorded in the index, with the fully-qualified module name appended as a `?module=` query parameter. The `data` field carries only the USR. There is no special URI scheme to parse. +> [!NOTE] +> The `file://path/to/module.{swiftinterface,swiftmodule}?module=` URL format is intended to become a general-purpose format used throughout sourcekit-lsp wherever a module file location with associated module name needs to be represented. It is currently only produced by `sourcekit/workspace/symbolInfo`. + `DocumentURI` equality and hashing use the filesystem path (via `withUnsafeFileSystemRepresentation`), which strips query parameters, so the URL with `?module=` compares equal to the clean path for all index and build-system lookups. ### `WorkspaceSymbol` fields From 139f6c757774c562e21e833f87106fda2717c19e Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Mon, 11 May 2026 17:01:43 -0700 Subject: [PATCH 10/13] Note that sourcekit/workspace/symbolInfo may return other URL schemes in the future --- Documentation/Open Quickly.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/Open Quickly.md b/Documentation/Open Quickly.md index 16ef49a15..5b2795502 100644 --- a/Documentation/Open Quickly.md +++ b/Documentation/Open Quickly.md @@ -139,6 +139,8 @@ When `sourcekit/workspace/symbolInfo` returns a `WorkspaceSymbol` for an SDK or > [!NOTE] > The `file://path/to/module.{swiftinterface,swiftmodule}?module=` URL format is intended to become a general-purpose format used throughout sourcekit-lsp wherever a module file location with associated module name needs to be represented. It is currently only produced by `sourcekit/workspace/symbolInfo`. +> +> In the future, `sourcekit/workspace/symbolInfo` may return other URL schemes. Clients should treat the location URI as opaque and pass it back to the server via `workspaceSymbol/resolve` without attempting to parse or interpret it. `DocumentURI` equality and hashing use the filesystem path (via `withUnsafeFileSystemRepresentation`), which strips query parameters, so the URL with `?module=` compares equal to the clean path for all index and build-system lookups. From 023a55e6ba743221667e4d7b346684a9807b414f Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Tue, 12 May 2026 15:07:51 -0700 Subject: [PATCH 11/13] Rename and revise "Jump to Definition" doc for SDK symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename to "Jump to Definition for SDK Symbols.md" and update the H1 - Reflow prose to ~120 columns - Remove outdated notes in §5 (USR fragment, scroll-without-getReferenceDocument) --- ... => Jump to Definition for SDK Symbols.md} | 108 +++++++----------- 1 file changed, 39 insertions(+), 69 deletions(-) rename Documentation/{Jump to Definition.md => Jump to Definition for SDK Symbols.md} (59%) diff --git a/Documentation/Jump to Definition.md b/Documentation/Jump to Definition for SDK Symbols.md similarity index 59% rename from Documentation/Jump to Definition.md rename to Documentation/Jump to Definition for SDK Symbols.md index 3356bf297..a13af12da 100644 --- a/Documentation/Jump to Definition.md +++ b/Documentation/Jump to Definition for SDK Symbols.md @@ -1,11 +1,10 @@ -# Jump to Definition +# Jump to Definition for SDK Symbols -Jump to definition for SDK/stdlib symbols works by generating a -textual Swift interface on demand and returning a `sourcekit-lsp://` -URI that the client can fetch via `workspace/getReferenceDocument`. +Jump to definition for SDK/stdlib symbols works by generating a textual Swift interface on demand and returning a +`sourcekit-lsp://` URI that the client can fetch via `workspace/getReferenceDocument`. -`sourcekit-lsp://` URIs are opaque. Clients must treat them as -identifiers and must not parse their structure to extract information. +`sourcekit-lsp://` URIs are opaque. Clients must treat them as identifiers and must not parse their structure to +extract information. ## Requests Involved @@ -16,15 +15,14 @@ identifiers and must not parse their structure to extract information. | `textDocument/didOpen` | Client → Server | Notify the server that the generated interface is open | | `textDocument/didClose` | Client → Server | Notify the server that the generated interface was closed | -`workspace/getReferenceDocument` is a SourceKit-LSP extension. The -client must advertise support in `ClientCapabilities.experimental`: +`workspace/getReferenceDocument` is a SourceKit-LSP extension. The client must advertise support in +`ClientCapabilities.experimental`: ```json { "workspace/getReferenceDocument": { "supported": true } } ``` -Without this capability the server writes the interface to a temporary -file and returns a `file://` URI instead. +Without this capability the server writes the interface to a temporary file and returns a `file://` URI instead. ## Workflow @@ -56,34 +54,26 @@ Client Server │ │ when it reaches 0] ``` -1. **Definition** — the client requests the definition of the symbol - at the cursor. For source-defined symbols the server returns a - `file://` URI with the exact source location. For SDK/stdlib - symbols it returns a `sourcekit-lsp://` URI and sets `range` to - the symbol's position within the generated interface (computed +1. **Definition** — the client requests the definition of the symbol at the cursor. For source-defined symbols the + server returns a `file://` URI with the exact source location. For SDK/stdlib symbols it returns a + `sourcekit-lsp://` URI and sets `range` to the symbol's position within the generated interface (computed server-side via `editor.find_usr`). -2. **Content retrieval** — the client fetches the generated interface - via `workspace/getReferenceDocument` to display its content. The - client scrolls to `range` from the definition response. -3. **Open notification** — once the client opens the tab it sends - `textDocument/didOpen` with the interface content (already fetched - in step 2) as `text`, `languageId` set to `"swift"`, and `version` - set to `1`. The server increments the ref count in - `GeneratedInterfaceManager`, keeping the interface open in - sourcekitd as long as the tab is open. -4. **Close notification** — when the client closes the tab it sends - `textDocument/didClose`, which decrements the ref count. When the - ref count reaches zero the interface is eligible for eviction from - the cache and will eventually be closed in sourcekitd. +2. **Content retrieval** — the client fetches the generated interface via `workspace/getReferenceDocument` to display + its content. The client scrolls to `range` from the definition response. +3. **Open notification** — once the client opens the tab it sends `textDocument/didOpen` with the interface content + (already fetched in step 2) as `text`, `languageId` set to `"swift"`, and `version` set to `1`. The server + increments the ref count in `GeneratedInterfaceManager`, keeping the interface open in sourcekitd as long as the + tab is open. +4. **Close notification** — when the client closes the tab it sends `textDocument/didClose`, which decrements the ref + count. When the ref count reaches zero the interface is eligible for eviction from the cache and will eventually be + closed in sourcekitd. ## Server-Side Flow ### 1. `textDocument/definition` handling -The server first attempts an index-based lookup -(`indexBasedDefinition`). For system/SDK symbols the index record -points to a `.swiftinterface` or `.swiftmodule` file, so the handler -calls: +The server first attempts an index-based lookup (`indexBasedDefinition`). For system/SDK symbols the index record +points to a `.swiftinterface` or `.swiftmodule` file, so the handler calls: ``` definitionInInterface( @@ -96,30 +86,21 @@ definitionInInterface( ### 2. `openGeneratedInterface` -`definitionInInterface` delegates to -`SwiftLanguageService.openGeneratedInterface`, which: +`definitionInInterface` delegates to `SwiftLanguageService.openGeneratedInterface`, which: -1. Constructs a fully-resolved `GeneratedInterfaceDocumentURLData` - using `init(moduleName:groupName:primaryFile:)`: - - `sourcekitdDocumentName` is synthesised as - `..` where `hash` is +1. Constructs a fully-resolved `GeneratedInterfaceDocumentURLData` using `init(moduleName:groupName:primaryFile:)`: + - `sourcekitdDocumentName` is synthesised as `..` where `hash` is `abs(buildSettingsFile.stringValue.hashValue)`. - - `buildSettingsFrom` is set to `originatorUri.buildSettingsFile` - — the build settings file of the **requesting source file**, not - the module file. This ensures sourcekitd uses the same compiler - arguments as the file that triggered the request. -2. Calls `generatedInterfaceManager.position(ofUsr:in:)` to find the - symbol's position within the generated interface (see below). -3. Returns `GeneratedInterfaceDetails(uri: sourcekit-lsp://..., - position: )`. - -The URI has no USR fragment. The position is returned separately and -used as `Location.range` in the definition response. + - `buildSettingsFrom` is set to `originatorUri.buildSettingsFile` — the build settings file of the **requesting + source file**, not the module file. This ensures sourcekitd uses the same compiler arguments as the file that + triggered the request. +2. Calls `generatedInterfaceManager.position(ofUsr:in:)` to find the symbol's position within the generated interface + (see below). +3. Returns `GeneratedInterfaceDetails(uri: sourcekit-lsp://..., position: )`. ### 3. Interface generation and caching -`GeneratedInterfaceManager` opens the interface in sourcekitd via -`editor.open.interface`: +`GeneratedInterfaceManager` opens the interface in sourcekitd via `editor.open.interface`: ``` keys.name: ".." @@ -129,28 +110,23 @@ keys.synthesizedExtension: 1 keys.compilerArgs: [... compiler arguments from build settings ...] ``` -The resulting `sourceText` is cached in memory keyed by -`sourcekitdDocumentName`. Subsequent requests for the same module + -build context reuse the cached snapshot. +The resulting `sourceText` is cached in memory keyed by `sourcekitdDocumentName`. Subsequent requests for the same +module + build context reuse the cached snapshot. ### 4. Symbol position within the interface -`GeneratedInterfaceManager.position(ofUsr:in:)` sends -`editor.find_usr` to sourcekitd: +`GeneratedInterfaceManager.position(ofUsr:in:)` sends `editor.find_usr` to sourcekitd: ``` keys.sourceFile: "" keys.usr: "" ``` -sourcekitd returns a byte offset, which is converted to a 0-based -`Position` via `DocumentSnapshot.positionOf(utf8Offset:)`. +sourcekitd returns a byte offset, which is converted to a 0-based `Position` via +`DocumentSnapshot.positionOf(utf8Offset:)`. ### 5. URI returned to the client -The `sourcekit-lsp://` URI is fully resolved — `sourcekitdDocument` -is always present, and there is no USR fragment: - ``` sourcekit-lsp://generated-swift-interface/Swift.String.swiftinterface ?moduleName=Swift @@ -159,18 +135,12 @@ sourcekit-lsp://generated-swift-interface/Swift.String.swiftinterface &buildSettingsFrom=file:///path/to/main.swift ``` -The `range` in the returned `Location` carries the symbol's position -in the interface, so the client knows where to scroll without calling -`workspace/getReferenceDocument` first. - ### 6. `workspace/getReferenceDocument` handling -The server extracts `buildSettingsFrom` from the URI to determine the -language service: +The server extracts `buildSettingsFrom` from the URI to determine the language service: ```swift primaryLanguageService(for: buildSettingsUri, ...).getReferenceDocument(req) ``` -`SwiftLanguageService.getReferenceDocument` retrieves the cached -interface snapshot. +`SwiftLanguageService.getReferenceDocument` retrieves the cached interface snapshot. From 9287f67fb6a551814499a09f102bbaa88a1771ba Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Tue, 12 May 2026 15:16:17 -0700 Subject: [PATCH 12/13] Reflow Open Quickly.md to 120 columns and note non-standard URI replacement - Reflow all prose to ~120 columns - Note that workspaceSymbol/resolve replaces location.uri (not just adds a range), which goes beyond what LSP 3.17 specifies --- Documentation/Open Quickly.md | 118 +++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 30 deletions(-) diff --git a/Documentation/Open Quickly.md b/Documentation/Open Quickly.md index 5b2795502..7075ab0f3 100644 --- a/Documentation/Open Quickly.md +++ b/Documentation/Open Quickly.md @@ -1,12 +1,15 @@ # Open Quickly -Open Quickly is a feature that lets editors provide fast symbol navigation across the entire workspace, including symbols defined in SDK `.swiftinterface` files. It is built on four LSP extensions that work together in a four-phase flow. +Open Quickly is a feature that lets editors provide fast symbol navigation across the entire workspace, including +symbols defined in SDK `.swiftinterface` files. It is built on four LSP extensions that work together in a four-phase +flow. ## LSP Extensions ### `sourcekit/workspace/symbolNames` — Discovery -Returns the flat list of every symbol name currently in the workspace index. The client uses this list to drive its search UI (fuzzy matching, prefix filtering, etc.). +Returns the flat list of every symbol name currently in the workspace index. The client uses this list to drive its +search UI (fuzzy matching, prefix filtering, etc.). ``` → WorkspaceSymbolNamesRequest {} @@ -17,7 +20,9 @@ Returns the flat list of every symbol name currently in the workspace index. The ### `sourcekit/workspace/symbolInfo` — Resolution -Given a list of names selected by the client after searching, returns structured location information for each name. Unlike the standard `workspace/symbol` request (which maps a query string to matching symbols), this request takes exact names and returns all occurrences. +Given a list of names selected by the client after searching, returns structured location information for each name. +Unlike the standard `workspace/symbol` request (which maps a query string to matching symbols), this request takes +exact names and returns all occurrences. The shape of each result item depends on the symbol's origin: @@ -39,7 +44,13 @@ The shape of each result item depends on the symbol's origin: } ``` -**SDK/stdlib symbols** — returned as `WorkspaceSymbol` with `location: .uri(file:// URL)` (no range) pointing to the `.swiftinterface` or `.swiftmodule` file from the index record, when the client advertises `workspace.symbol.resolveSupport.properties` containing `"location"` or both `"location.uri"` and `"location.range"`. The fully-qualified module name (e.g. `Swift.String`) is appended as a `?module=` query parameter on the location URL so clients can derive a display path without inspecting the `data` dictionary. The symbol's USR is stored in the `data` dictionary. The client must call `workspaceSymbol/resolve` to obtain the exact location within the generated interface. +**SDK/stdlib symbols** — returned as `WorkspaceSymbol` with `location: .uri(file:// URL)` (no range) pointing to the +`.swiftinterface` or `.swiftmodule` file from the index record, when the client advertises +`workspace.symbol.resolveSupport.properties` containing `"location"` or both `"location.uri"` and `"location.range"`. +The fully-qualified module name (e.g. `Swift.String`) is appended as a `?module=` query parameter on the location URL +so clients can derive a display path without inspecting the `data` dictionary. The symbol's USR is stored in the +`data` dictionary. The client must call `workspaceSymbol/resolve` to obtain the exact location within the generated +interface. ``` → WorkspaceSymbolInfoRequest { names: ["String"] } @@ -55,13 +66,23 @@ The shape of each result item depends on the symbol's origin: } ``` -Without that capability, the raw `file://` URI of the `.swiftinterface` or `.swiftmodule` file from the index record is returned as `SymbolInformation` instead. +Without that capability, the raw `file://` URI of the `.swiftinterface` or `.swiftmodule` file from the index record +is returned as `SymbolInformation` instead. The response is a flat array of `WorkspaceSymbolItem` values. Each item carries the symbol name in its `name` field. ### `workspaceSymbol/resolve` — Location Resolution -Resolves the lazy location of a `WorkspaceSymbol` returned by `sourcekit/workspace/symbolInfo`. The server parses `moduleName` and `groupName` from the `?module=` query parameter of the location URL, reads `usr` from the `data` dictionary, opens the generated Swift interface for the symbol's module, finds the symbol's position using the USR, and returns the same symbol with `location` replaced by a full `Location` (URI + range). +Resolves the lazy location of a `WorkspaceSymbol` returned by `sourcekit/workspace/symbolInfo`. The server parses +`moduleName` and `groupName` from the `?module=` query parameter of the location URL, reads `usr` from the `data` +dictionary, opens the generated Swift interface for the symbol's module, finds the symbol's position using the USR, +and returns the same symbol with `location` replaced by a full `Location` (URI + range). + +> [!NOTE] +> The LSP 3.17 spec defines `workspaceSymbol/resolve` as filling in a missing `range` for a URI-only location — it +> does not anticipate replacing `location.uri` itself. SourceKit-LSP goes beyond the spec here: it substitutes the +> `file://` module-file URL with a `sourcekit-lsp://` URI that the client can pass to `workspace/getReferenceDocument` +> to retrieve the generated interface content. ``` → WorkspaceSymbolResolveRequest { @@ -82,13 +103,19 @@ Resolves the lazy location of a `WorkspaceSymbol` returned by `sourcekit/workspa } ``` -The resolved URI is a fully-parameterized `sourcekit-lsp://generated-swift-interface/` URL containing `sourcekitdDocument` and `buildSettingsFrom` derived from a real source file in the workspace via `mainFiles(containing:)`. +The resolved URI is a fully-parameterized `sourcekit-lsp://generated-swift-interface/` URL containing +`sourcekitdDocument` and `buildSettingsFrom` derived from a real source file in the workspace via +`mainFiles(containing:)`. -The client must treat the resolved `sourcekit-lsp://` URI as **opaque** — it should not parse or extract information from the query parameters. The path component (e.g. `Swift.String.swiftinterface`) may be used as the editor tab title. The URI is otherwise only valid as an input to `workspace/getReferenceDocument`; its query parameter structure is an implementation detail subject to change. +The client must treat the resolved `sourcekit-lsp://` URI as **opaque** — it should not parse or extract information +from the query parameters. The path component (e.g. `Swift.String.swiftinterface`) may be used as the editor tab +title. The URI is otherwise only valid as an input to `workspace/getReferenceDocument`; its query parameter structure +is an implementation detail subject to change. ### `workspace/getReferenceDocument` — Content Retrieval -Fetches the text content of a reference document URI (e.g. a generated Swift interface). This is a pure content provider — it returns the document text and nothing else. +Fetches the text content of a reference document URI (e.g. a generated Swift interface). This is a pure content +provider — it returns the document text and nothing else. ``` → GetReferenceDocumentRequest { uri: "sourcekit-lsp://generated-swift-interface/...?sourcekitdDocument=...&..." } @@ -97,7 +124,8 @@ Fetches the text content of a reference document URI (e.g. a generated Swift int } ``` -The URI passed here must be a fully resolved URI (with `sourcekitdDocument` set), as returned by `workspaceSymbol/resolve`. +The URI passed here must be a fully resolved URI (with `sourcekitdDocument` set), as returned by +`workspaceSymbol/resolve`. ## Workflow @@ -127,22 +155,36 @@ Client Server ``` 1. **Discovery** — fetch all names; client filters locally. -2. **Resolution** — send matching name(s) to populate the search result list; server returns symbol details (kind, container name, location) for display. +2. **Resolution** — send matching name(s) to populate the search result list; server returns symbol details (kind, + container name, location) for display. - Source symbols: `SymbolInformation` with a `file://` URI and exact position. No further steps required. - - SDK/stdlib symbols: `WorkspaceSymbol` with `location: .uri(file:// URL?module=...)` pointing to the module file and the USR in `data["usr"]`, when the client advertises `workspace.symbol.resolveSupport.properties` containing `"location"` or both `"location.uri"` and `"location.range"`. Otherwise falls back to `SymbolInformation` with the raw `file://` URI. -3. **Location resolution** — call `workspaceSymbol/resolve` with the selected `WorkspaceSymbol` to open the generated interface and resolve the symbol position. The server synthesizes the final `sourcekit-lsp://` URI and fills in `location.range`. -4. **Content retrieval** — fetch the generated interface text. The editor scrolls to `location.range.start` from the resolve step. + - SDK/stdlib symbols: `WorkspaceSymbol` with `location: .uri(file:// URL?module=...)` pointing to the module file + and the USR in `data["usr"]`, when the client advertises `workspace.symbol.resolveSupport.properties` containing + `"location"` or both `"location.uri"` and `"location.range"`. Otherwise falls back to `SymbolInformation` with + the raw `file://` URI. +3. **Location resolution** — call `workspaceSymbol/resolve` with the selected `WorkspaceSymbol` to open the generated + interface and resolve the symbol position. The server synthesizes the final `sourcekit-lsp://` URI and fills in + `location.range`. +4. **Content retrieval** — fetch the generated interface text. The editor scrolls to `location.range.start` from the + resolve step. ## Pre-resolve Location Design for SDK/stdlib Symbols -When `sourcekit/workspace/symbolInfo` returns a `WorkspaceSymbol` for an SDK or stdlib symbol, the location is a `file://` URL pointing to the `.swiftinterface` or `.swiftmodule` file recorded in the index, with the fully-qualified module name appended as a `?module=` query parameter. The `data` field carries only the USR. There is no special URI scheme to parse. +When `sourcekit/workspace/symbolInfo` returns a `WorkspaceSymbol` for an SDK or stdlib symbol, the location is a +`file://` URL pointing to the `.swiftinterface` or `.swiftmodule` file recorded in the index, with the +fully-qualified module name appended as a `?module=` query parameter. The `data` field carries only the USR. There is +no special URI scheme to parse. > [!NOTE] -> The `file://path/to/module.{swiftinterface,swiftmodule}?module=` URL format is intended to become a general-purpose format used throughout sourcekit-lsp wherever a module file location with associated module name needs to be represented. It is currently only produced by `sourcekit/workspace/symbolInfo`. +> The `file://path/to/module.{swiftinterface,swiftmodule}?module=` URL format is intended to become a +> general-purpose format used throughout sourcekit-lsp wherever a module file location with associated module name +> needs to be represented. It is currently only produced by `sourcekit/workspace/symbolInfo`. > -> In the future, `sourcekit/workspace/symbolInfo` may return other URL schemes. Clients should treat the location URI as opaque and pass it back to the server via `workspaceSymbol/resolve` without attempting to parse or interpret it. +> In the future, `sourcekit/workspace/symbolInfo` may return other URL schemes. Clients should treat the location URI +> as opaque and pass it back to the server via `workspaceSymbol/resolve` without attempting to parse or interpret it. -`DocumentURI` equality and hashing use the filesystem path (via `withUnsafeFileSystemRepresentation`), which strips query parameters, so the URL with `?module=` compares equal to the clean path for all index and build-system lookups. +`DocumentURI` equality and hashing use the filesystem path (via `withUnsafeFileSystemRepresentation`), which strips +query parameters, so the URL with `?module=` compares equal to the clean path for all index and build-system lookups. ### `WorkspaceSymbol` fields @@ -153,7 +195,8 @@ When `sourcekit/workspace/symbolInfo` returns a `WorkspaceSymbol` for an SDK or ### `?module=` query parameter -The `?module=` value is the fully-qualified dotted module name recorded in the index (e.g. `Swift.String`, `Foundation`). The server appends it when constructing the location URL: +The `?module=` value is the fully-qualified dotted module name recorded in the index (e.g. `Swift.String`, +`Foundation`). The server appends it when constructing the location URL: ```swift var urlComponents = URLComponents(string: moduleFileURI.stringValue)! @@ -185,17 +228,20 @@ WorkspaceSymbol { ### `workspaceSymbol/resolve` transformation -The resolve step parses the location URL, extracts the clean file path (query excluded via `urlComponents.path`) for `mainFiles(containing:)`, then opens the generated interface via sourcekitd: +The resolve step parses the location URL, extracts the clean file path (query excluded via `urlComponents.path`) for +`mainFiles(containing:)`, then opens the generated interface via sourcekitd: 1. Parse `?module=Swift.String` from `uriOnly.uri.arbitrarySchemeURL`; split at first `.` → `moduleName`, `groupName` 2. Read `usr` from `data["usr"]` -3. Look up a real source file via `mainFiles(containing: moduleFileURI)`, sorted by URL string for determinism; pick `.first` +3. Look up a real source file via `mainFiles(containing: moduleFileURI)`, sorted by URL string for determinism; pick + `.first` 4. Call `openGeneratedInterface(document: primaryFile, moduleName:, groupName:, symbolUSR:)` 5. Return the symbol with `location` replaced by a full `Location` (resolved `sourcekit-lsp://` URI + range) ## `sourcekit-lsp://` URI for Resolved Locations -After `workspaceSymbol/resolve`, the location URI is a fully-parameterized `sourcekit-lsp://generated-swift-interface/` URL. +After `workspaceSymbol/resolve`, the location URI is a fully-parameterized `sourcekit-lsp://generated-swift-interface/` +URL. ### URL Structure @@ -236,12 +282,24 @@ sourcekit-lsp://generated-swift-interface/Swift.String.swiftinterface ## Notes -- _User_ binary `.swiftmodule` files compiled without `-index-store-path` are **not** indexed — there is no index store record for them, so their symbols do not appear in `sourcekit/workspace/symbolNames` or `sourcekit/workspace/symbolInfo`. -- _System/non-user_ binary modules (`isNonUserModule() == true`) **are** indexed by the Swift compiler when `indexSystemModules` is enabled (`IndexRecord.cpp: emitDataForSwiftSerializedModule`): - - *Resilient* system modules: the compiler reloads from the adjacent `.swiftinterface` before indexing. If no interface is available, the module is skipped entirely. - - *Non-resilient* system modules and the stdlib: indexed directly from the binary; symbol locations in the index point to the `.swiftmodule` file. -- Both `.swiftinterface` and `.swiftmodule` location paths are handled identically in `workspaceSymbolItem` — both produce a `WorkspaceSymbol` with a `file://` location URI (carrying `?module=`) and a `data` dictionary with the USR, when the client has the required capabilities. sourcekitd can synthesize a textual interface from either form. +- _User_ binary `.swiftmodule` files compiled without `-index-store-path` are **not** indexed — there is no index + store record for them, so their symbols do not appear in `sourcekit/workspace/symbolNames` or + `sourcekit/workspace/symbolInfo`. +- _System/non-user_ binary modules (`isNonUserModule() == true`) **are** indexed by the Swift compiler when + `indexSystemModules` is enabled (`IndexRecord.cpp: emitDataForSwiftSerializedModule`): + - *Resilient* system modules: the compiler reloads from the adjacent `.swiftinterface` before indexing. If no + interface is available, the module is skipped entirely. + - *Non-resilient* system modules and the stdlib: indexed directly from the binary; symbol locations in the index + point to the `.swiftmodule` file. +- Both `.swiftinterface` and `.swiftmodule` location paths are handled identically in `workspaceSymbolItem` — both + produce a `WorkspaceSymbol` with a `file://` location URI (carrying `?module=`) and a `data` dictionary with the + USR, when the client has the required capabilities. sourcekitd can synthesize a textual interface from either form. - One client capability gates the `WorkspaceSymbol`/`.uri` path in `sourcekit/workspace/symbolInfo`: - - `ClientCapabilities.workspace.symbol.resolveSupport.properties` containing `"location"` or both `"location.uri"` and `"location.range"` (LSP 3.17) — signals that the client can call `workspaceSymbol/resolve` to obtain a range-bearing location. - - Without it, `sourcekit/workspace/symbolInfo` returns `SymbolInformation` with the raw `file://` URI from the index record. -- The resolved location URI from `workspaceSymbol/resolve` uses a `sourcekit-lsp://generated-swift-interface/` scheme when the client advertises `GetReferenceDocumentRequest` support, or a temp `file://` path otherwise. Both forms can be used to open the generated interface. + - `ClientCapabilities.workspace.symbol.resolveSupport.properties` containing `"location"` or both `"location.uri"` + and `"location.range"` (LSP 3.17) — signals that the client can call `workspaceSymbol/resolve` to obtain a + range-bearing location. + - Without it, `sourcekit/workspace/symbolInfo` returns `SymbolInformation` with the raw `file://` URI from the + index record. +- The resolved location URI from `workspaceSymbol/resolve` uses a `sourcekit-lsp://generated-swift-interface/` scheme + when the client advertises `GetReferenceDocumentRequest` support, or a temp `file://` path otherwise. Both forms can + be used to open the generated interface. From 5bc9f3a6f24c7535444411408de23c350d66bd22 Mon Sep 17 00:00:00 2001 From: Rintaro Ishizaki Date: Tue, 12 May 2026 15:44:37 -0700 Subject: [PATCH 13/13] Review updates --- Sources/SourceKitLSP/SourceKitLSPServer.swift | 6 ++++-- Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 2d18ad978..211c70afa 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -1877,7 +1877,7 @@ extension SourceKitLSPServer { for name in req.names { if Task.isCancelled { return [:] } var symbols: [SymbolOccurrence] = [] - _ = orLog("getting symbol information") { + _ = orLog("Getting symbol occurrences") { try index.forEachCanonicalSymbolOccurrence(byName: name) { symbolOccurrence in symbols.append(symbolOccurrence) return true @@ -1885,7 +1885,7 @@ extension SourceKitLSPServer { } if Task.isCancelled { return [:] } result[name] = symbols.compactMap { symbol in - orLog("getting symbol information") { + orLog("Getting symbol information") { try self.workspaceSymbolItem( for: symbol, in: index, @@ -1898,6 +1898,8 @@ extension SourceKitLSPServer { return result } + try Task.checkCancellation() + // Flatten the result. var result: [WorkspaceSymbolItem] = [] for name in req.names { diff --git a/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift b/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift index 7e239bce4..ccda576c2 100644 --- a/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceSymbolInfoTests.swift @@ -129,7 +129,7 @@ final class WorkspaceSymbolInfoTests: XCTestCase { // so its symbols are never written to any index store. try await withTestScratchDir { binaryModuleDir in let sourceFile = binaryModuleDir.appendingPathComponent("BinaryLib.swift") - try "public struct BinaryOnlyStruct {}".write(to: sourceFile, atomically: true, encoding: .utf8) + try await "public struct BinaryOnlyStruct {}".writeWithRetry(to: sourceFile) var args = [ swiftc.path,