Skip to content

Emit a stable generated-interface URI for SDK workspace symbols#2730

Open
rintaro wants to merge 1 commit into
swiftlang:mainfrom
rintaro:stable-generated-interface-uri
Open

Emit a stable generated-interface URI for SDK workspace symbols#2730
rintaro wants to merge 1 commit into
swiftlang:mainfrom
rintaro:stable-generated-interface-uri

Conversation

@rintaro

@rintaro rintaro commented Jul 21, 2026

Copy link
Copy Markdown
Member

workspace/symbolInfo returned SDK/stdlib symbols with a deferred file://<interface>?module=<name> location that workspaceSymbol/resolve replaced with a sourcekit-lsp:// reference-document URI. Replacing the URI on resolve requires the client to advertise location.uri resolution, so clients that resolve only location.range (e.g. VS Code) couldn't use it.

This emits the sourcekit-lsp://generated-swift-interface URI up front instead. It's self-describing, shared by every symbol of the module, and workspaceSymbol/resolve now leaves it untouched and only fills in the range, extending the feature to clients with location.range-only resolution and avoiding the percent-encoding issue via the custom scheme.

The interface's main file (for build settings) is resolved via mainFiles(containing:) and memoized per interface, so the number of mainFiles(containing:) calls is bounded by the distinct system modules imported in the workspace, not the number of symbols in the response.

The WorkspaceSymbol's data now also carries the .swiftinterface/.swiftmodule path and the fully-qualified module name, so clients can render the candidate without parsing the opaque location URI.

The path is gated on the client supporting both generated-interface reference documents and workspaceSymbol/resolve; otherwise it falls back to a plain file:// interface location. The server now advertises workspaceSymbolProvider with resolveProvider: true, and clientSupportsWorkspaceSymbolResolve is relaxed to accept location or location.range. Only workspace/symbolInfo is affected — workspace/symbol filters out system symbols.

@rintaro

rintaro commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@swift-ci Please test

Comment thread Sources/SourceKitLSP/MessageMetadata.swift Outdated
@rintaro
rintaro force-pushed the stable-generated-interface-uri branch from 16a0d4a to 201d47b Compare July 21, 2026 21:32

@ahoppen ahoppen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the stable URI approach a lot better 🙂

Comment on lines +133 to +135
/// The location's URI is always sent up front and is left unchanged by `workspaceSymbol/resolve`, which
/// only fills in the range. So it suffices for the client to resolve `location` or just `location.range`;
/// resolving `location.uri` is not required.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure if explicitly spelling out what's not required makes too much sense here. If we start spelling out what’s not required in every doc comment, the documentation would explode.

/// render the candidate in their UI without parsing the opaque location URI.
var moduleName: String?

init(usr: String, interfaceURI: DocumentURI? = nil, moduleName: String? = nil) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we omit the default values here so callers can’t just forget to pass interfaceURI and moduleName?

Comment on lines +1645 to +1646
private nonisolated static func splitModuleNameAndGroup(_ fullModuleName: String) -> (module: String, group: String?)
{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private nonisolated static func splitModuleNameAndGroup(_ fullModuleName: String) -> (module: String, group: String?)
{
private nonisolated static func splitModuleNameAndGroup(
_ fullModuleName: String
) -> (module: String, group: String?) {

Comment on lines +100 to +102
The location URI is **unchanged** — resolve only adds the range. This matches the LSP 3.17 contract for
`workspaceSymbol/resolve` (fill in a missing `range` for a URI-only location) and means the client only needs to
advertise resolution of `location` or `location.range`; it does not need to advertise `location.uri`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think this is worth explicitly spelling out. Not changing the URI is standard behavior in LSP.


After `workspaceSymbol/resolve`, the location URI is a fully-parameterized `sourcekit-lsp://generated-swift-interface/`
URL.
The location URI for an SDK/stdlib `WorkspaceSymbol` is a fully-parameterized

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar here. I don’t think we need to keep spelling out that URI is unchanged.

else {
continue
}
if Task.isCancelled { return [:] }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we try Task.checkCancellation() here to avoid callers unintentionally using an empty dictionary as a valid result if they don’t end up checking cancellation themselves? I don’t see a reason why this function wouldn’t be able to throw.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason was that this is called inside concurrentMap. But I'm going to wrap it with orLog in the caller.

location: .uri(.init(uri: DocumentURI(locationURL))),
data: data
location: .uri(.init(uri: try urlData.uri)),
data: data.encodeToLSPAny()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we continue to pass nil here if the encoded data is empty? Just thinking that omitting data: altogether will result in a smaller JSON response than having data: {}.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, we passed nil iff usr.isEmpty but that shouldn't reallly happen. So I don't think it's worth to add explicit checking.

/// If the symbol has a `location: .uri(sourcekit-lsp://generated-swift-interface?...)` (as emitted by
/// `workspace/symbol` and `workspace/symbolInfo` for SDK/stdlib symbols), opens the generated Swift
/// interface, resolves the symbol position using `data["usr"]`, and returns the symbol with the exact
/// range. The URI is kept unchanged so it stays stable for the client; only the range is filled in.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, not sure if it’s worth spelling out that URI stays unchanged.

Location(uri: details.uri, range: Range(details.position ?? Position(line: 0, utf16index: 0)))
)
}
// Treat an empty USR as absent so we don't run a position lookup that can't match.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you see this happen somewhere in practice?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. This is just a defensive guard.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, maybe worth spelling this out.

Comment thread Sources/SourceKitLSP/MessageMetadata.swift Outdated
@rintaro
rintaro force-pushed the stable-generated-interface-uri branch 2 times, most recently from b6ce4fe to f4fd108 Compare July 23, 2026 20:57
@rintaro
rintaro force-pushed the stable-generated-interface-uri branch 3 times, most recently from cd7e11f to 2329630 Compare July 23, 2026 21:49
`workspace/symbolInfo` returned SDK/stdlib symbols with a deferred
`file://<interface>?module=<name>` location that `workspaceSymbol/resolve`
*replaced* with a `sourcekit-lsp://` reference-document URI. Replacing the
URI on resolve requires the client to advertise `location.uri` resolution,
so clients that resolve only `location.range` (e.g. VS Code) couldn't use
it — and routing the module name through a `file://` query broke when
clients percent-encoded `=`.

Emit the final `sourcekit-lsp://generated-swift-interface` URI up front
instead, so it stays stable for the rest of the flow: `workspaceSymbol/resolve`
only fills in the range (leaving the URI unchanged) and
`workspace/getReferenceDocument` returns the content. Because the URI no
longer changes during resolve, clients only need to resolve `location` or
`location.range`, and riding on the custom scheme also avoids the
percent-encoding problem.

Carry a `SourceKitWorkspaceSymbolData` in the symbol's `data` (USR plus the
interface path and module name), which clients decode via
`WorkspaceSymbol.sourceKitData` to render the candidate without inspecting
the opaque URI.

The interface's main file (for build settings) is resolved via
`mainFiles(containing:)` and memoized per interface, so the number of
`mainFiles(containing:)` calls is bounded by the distinct system modules
imported in the workspace, not the number of symbols in the response.

The path is gated on the client supporting both generated-interface
reference documents and `workspaceSymbol/resolve`; otherwise it falls back
to a plain `file://` interface location. The server now advertises
`workspaceSymbolProvider` with `resolveProvider: true`, and
`clientSupportsWorkspaceSymbolResolve` is relaxed to accept `location` or
`location.range` (resolving `location.uri` is no longer required, since the
URI is stable across resolve). Only `workspace/symbolInfo` is affected —
`workspace/symbol` filters out system symbols.

Document the extension in `Contributor Documentation/LSP Extensions.md` and
`Open Quickly.md`.
@rintaro
rintaro force-pushed the stable-generated-interface-uri branch from 2329630 to c2d9825 Compare July 23, 2026 21:59
@rintaro

rintaro commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

swiftlang/swift-tools-protocols#75
@swift-ci Please test

@rintaro

rintaro commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

swiftlang/swift-tools-protocols#75
@swift-ci Please test Windows

@ahoppen ahoppen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just have the following comments open. Feel free to merge after addressing them:

Comment on lines +1643 to +1645
/// Split an index module name of the form `Module` or `Module.Group` into its module and optional group
/// components, on the first `.`.
private nonisolated static func splitModuleNameAndGroup(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this isn't new with this PR, but this doesn't seem like it will handle submodules correctly? I guess we need to be properly tracking that in the index data?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants