Emit a stable generated-interface URI for SDK workspace symbols#2730
Emit a stable generated-interface URI for SDK workspace symbols#2730rintaro wants to merge 1 commit into
Conversation
|
@swift-ci Please test |
16a0d4a to
201d47b
Compare
ahoppen
left a comment
There was a problem hiding this comment.
I like the stable URI approach a lot better 🙂
| /// 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. |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Should we omit the default values here so callers can’t just forget to pass interfaceURI and moduleName?
| private nonisolated static func splitModuleNameAndGroup(_ fullModuleName: String) -> (module: String, group: String?) | ||
| { |
There was a problem hiding this comment.
| private nonisolated static func splitModuleNameAndGroup(_ fullModuleName: String) -> (module: String, group: String?) | |
| { | |
| private nonisolated static func splitModuleNameAndGroup( | |
| _ fullModuleName: String | |
| ) -> (module: String, group: String?) { |
| 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`. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Similar here. I don’t think we need to keep spelling out that URI is unchanged.
| else { | ||
| continue | ||
| } | ||
| if Task.isCancelled { return [:] } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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: {}.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Did you see this happen somewhere in practice?
There was a problem hiding this comment.
I don't think so. This is just a defensive guard.
There was a problem hiding this comment.
OK, maybe worth spelling this out.
b6ce4fe to
f4fd108
Compare
cd7e11f to
2329630
Compare
`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`.
2329630 to
c2d9825
Compare
|
swiftlang/swift-tools-protocols#75 |
|
swiftlang/swift-tools-protocols#75 |
ahoppen
left a comment
There was a problem hiding this comment.
LGTM, just have the following comments open. Feel free to merge after addressing them:
| /// 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( |
There was a problem hiding this comment.
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?
workspace/symbolInforeturned SDK/stdlib symbols with a deferredfile://<interface>?module=<name>location thatworkspaceSymbol/resolvereplaced with asourcekit-lsp://reference-document URI. Replacing the URI on resolve requires the client to advertiselocation.uriresolution, so clients that resolve onlylocation.range(e.g. VS Code) couldn't use it.This emits the
sourcekit-lsp://generated-swift-interfaceURI up front instead. It's self-describing, shared by every symbol of the module, andworkspaceSymbol/resolvenow leaves it untouched and only fills in the range, extending the feature to clients withlocation.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 ofmainFiles(containing:)calls is bounded by the distinct system modules imported in the workspace, not the number of symbols in the response.The
WorkspaceSymbol'sdatanow also carries the.swiftinterface/.swiftmodulepath 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 plainfile://interface location. The server now advertisesworkspaceSymbolProviderwithresolveProvider: true, andclientSupportsWorkspaceSymbolResolveis relaxed to acceptlocationorlocation.range. Onlyworkspace/symbolInfois affected —workspace/symbolfilters out system symbols.