Add document links for every tsconfig path - #334894
Add document links for every tsconfig path#334894Andrei L (unrevised6419) wants to merge 5 commits into
Conversation
`TsconfigLinkProvider` linked three fields: `extends`, `files`, and `references[].path`. Every other value in a project config that names a file or a directory was inert, and `extends` only worked in its string form, so the array form TypeScript 5.0 introduced was silently dropped. Eighteen contribution rows now cover those three plus `include`, `exclude`, `compilerOptions.lib`, `types`, `typeRoots`, `rootDir`, `rootDirs`, `baseUrl`, `outDir`, `declarationDir`, `outFile`, `tsBuildInfoFile`, `mapRoot`, and `sourceRoot`. The provider is table-driven rather than a method per field, so a future field is one row and a future resolution strategy is a row plus a function. `links.ts` maps JSON paths to link kinds and produces candidates without touching the file system, which makes it directly testable. `resolvers.ts` turns a kind and a string into a target URI. `index.ts` registers the provider and opens what a resolver returns. Notable behaviors: - `include` and `exclude` entries are usually globs, so only the literal leading segments are underlined and followed: `src/**/*` links `src`. - Directory targets reveal in the explorer, since an editor cannot open a folder. - `lib` entries resolve against the TypeScript the language service is actually using, honoring `typescript.tsdk` and the workspace version picker, falling back to the bundled copy. - A missing target still offers to create the file for the kinds that name files, and reports for the kinds that name directories. `TypeScriptVersionManager` decided which install is active inside its constructor. That rule moves to an exported function so the `lib` resolver can ask the same question, rather than reading the manager's storage key and reimplementing it. The version provider is configured on first use rather than at activation, because loading the configuration can shell out synchronously. Left for later, each one a row plus at most a resolver: `compilerOptions.paths` values, which resolve against `baseUrl` and need document context the resolver contract does not carry; `plugins[].name` and `jsxImportSource`, which are package resolution; `watchOptions`; and the deprecated `out`.
3c7364e to
874261d
Compare
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
There was a problem hiding this comment.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Note
This error may be related to your runner configuration. You can now configure runners for Copilot code review separately from Copilot cloud agent by creating a copilot-code-review.yml file with your setup steps. Read the docs for details.
|
Initial review notes, ordered by impact. Line references are against the current head of the branch. Bugs
Quality
🤖 Generated with Claude Code |
- A glob whose only literal prefix is `.` (`./*.ts`) produced a one-character link that revealed the folder the config already lives in. It is no longer linked. - `types` entries and the other path resolvers now classify relative and absolute paths the way the compiler does, on every platform: `.`, `..`, `.\foo`, `\\server\share` and `C:\foo` are paths, not package names, and the check no longer depends on `path.isAbsolute` of the host platform. - `lib` resolution read `bundledVersion` twice per click. The getter rescans disk and shows an error toast before throwing, so a missing bundled install produced two identical toasts. It is now read once. - A directory target outside the workspace was handed to `revealInExplorer`, which silently does nothing for it. It is now revealed in the OS file manager on desktop, or reported with a message elsewhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WadC2xvsDdvGEyyV4ia2B8
…able The version provider used by `lib` links was configured lazily on the first click through the full `loadFromWorkspace()`, which reads every TypeScript setting and, when `tsserver.nodePath` is "node", synchronously spawns `node` and may show Node warnings unrelated to the link. It also ran only once, so with the native preview enabled a later change to `typescript.tsdk` was ignored until reload. `loadTsdkFromWorkspace()` now reads just the two tsdk settings, which is all the provider consumes, and is cheap enough to run at activation and again whenever those settings change. Each link kind's resolver, failure wording, and missing-target policy were spread over a resolver record and two switches. They now live in one descriptor record, so adding a kind is one entry rather than three places, and no kind inherits wording from a `default` arm. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WadC2xvsDdvGEyyV4ia2B8
…em on web A `lib` entry was mapped straight to `lib.<name>.d.ts`, but TypeScript resolves it through its `libMap`, where aliases such as `ES7` or `ESNext.BigInt` point at the file of the edition that shipped the feature. Those entries compiled fine yet reported "Failed to resolve TypeScript lib". The resolver now consults the lib map of each candidate install and nothing else, so an entry the map does not know is not a lib, however plausible the file name would look. On desktop the map is read from the `typescript.js` beside the install's `tsserver.js`, cached per install, and an install inside the workspace is only loaded once the workspace is trusted, the same rule tsserver follows. On web the bundle step writes `libMap.json` next to the lib files it already copies, from the same TypeScript, so the two never drift. On web the lib files are served over http(s), where the workbench's fetch provider answers every `stat` with a file and `vscode.open` hands the URL to the browser, which offered to download it. Existence is now checked with a real read, and http(s) targets open as text documents. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WadC2xvsDdvGEyyV4ia2B8
Link ranges are now mapped through the source text a value was decoded from, so a narrowed link inside an escaped value points at the characters it names instead of being dropped, and a string whose closing quote has not been typed yet is no longer underlined one character short. Glob prefixes accept either separator, the way the compiler normalizes them. A path written with Windows separators now resolves the same on every platform: values pass through `normalizeSlashes` before reaching a URI, since `Uri.file` and `Uri.joinPath` only treat a backslash as a separator on Windows. `looksLikeRelativePath` and `looksLikeAbsolutePath` move beside the other path predicates in `utils/fs`, along with a shared `tryStat`. A `lib` entry now resolves through the file the install ships whenever one is named for it, which is all but the aliases, so the common case no longer loads the install's compiler on the extension host thread. Only a value shaped like a lib name is looked up that way, so a value carrying a separator cannot point the link out of the install. The map read that the aliases still need caches failures too, keys the cache by the modification time of the `typescript.js` it read, so an install replaced in place is picked up, and drops the module again once the map is taken from it. An install the service could not read is skipped, matching the fallback `TypeScriptVersionManager.reset` performs. On web, a lib file is no longer downloaded once to prove it exists and again to show it: the lib map already says the file ships with that install. A folder outside the workspace reports rather than calling `revealFileInOS` in a remote window, where that command does nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N6zKNmBMmzGbtjX3C4pWkj
Description
TsconfigLinkProviderlinks three fields today:extends,filesandreferences[].path. Every other value in a project config that names a file or a directory is inert.extendsalso only works in its string form, so the array form TypeScript 5.0 introduced is silently dropped: the array node is handed to a check that requires a string node, and every entry disappears.This adds links for the rest. Eighteen contribution rows cover the existing three plus
include,exclude,compilerOptions.lib,types,typeRoots,rootDir,rootDirs,baseUrl,outDir,declarationDir,outFile,tsBuildInfoFile,mapRootandsourceRoot.Demo
https://youtu.be/L0CBEplaOtc
Approach
The provider is table-driven rather than a method per field, because the previous shape meant every new field edited the same three methods. Adding a field is now one row, and adding a resolution strategy is a row plus a function.
languageFeatures/tsconfig/holds four pieces:links.tsmaps JSON paths to link kinds and produces link candidates. It performs no file system access, which is what makes it directly testable. A path segment can be a symbol wildcard matching every array element, soextendsis expressed as two rows and both its forms work with no branching.resolvers.tsdescribes each kind in one record: how it resolves to a target URI, what it says when it cannot, and what to do when the target does not exist. The existingextendsandreferencesresolution moves here, with its path-versus-module test aligned with the compiler's, so.\foo,..,\\server\shareandC:\foocount as paths on every platform instead of depending on the host'spath.isAbsolute. Separators are normalized the way the compiler does before a value reaches a URI, sinceUri.fileandUri.joinPathonly read a backslash as a separator on Windows, so a path written on Windows resolves the same everywhere.libMap.ts, with an.electron.tsand a.browser.tsimplementation, reads a TypeScript install'slibMap, which is what turns alibentry into a file name.index.tsregisters the provider and the command, and carries out the outcome: open, reveal, or report.Behaviors worth calling out:
includeandexcludeentries are usually globs, so only the literal leading segments are underlined and followed.src/**/*linkssrc. An entry that begins with a wildcard has no directory to point at and gets no link, and neither does./*.ts, whose only literal segment is the config's own directory.vscode.opencannot open a folder in an editor. This applies totypeRoots,rootDir,baseUrland glob prefixes. A directory outside the workspace cannot be selected in the explorer, so it is revealed in the OS file manager in a local desktop window and reported with a message elsewhere, remote windows included, where that command has no local path to show.libentries resolve against the TypeScript the language service is actually using, honoringtypescript.tsdkand the workspace version picker, then any other local install, then the bundled copy. The file comes from the install rather than from the entry's spelling: a name the install ships alib.<name>.d.tsfor resolves to that file, and every other name, which is to say the aliases, goes through the install's ownlibMap, soES7andESNext.BigIntopenlib.es2016.d.tsandlib.es2020.bigint.d.tsexactly as the compiler would, and a name neither knows is reported. An install the service could not read is skipped, the same fallback the version manager makes. On desktop the map is read from thetypescript.jsbeside the install'stsserver.js, which is why it is consulted only for the aliases: loading it is expensive, so the result is cached against that file's modification time and the module is dropped again once the map is taken from it. An install inside the workspace is only loaded once the workspace is trusted, the same rule tsserver follows. On web the bundle step writeslibMap.jsonnext to thelib.*.d.tsfiles it already copies, from the same TypeScript, and the lib opens as a readonly text document rather than being handed to the browser.typesentries that are paths rather than package names, such as./typings/foo, resolve as paths."rootDir": "./src"no longer offers to create a file calledsrc.mapRootandsourceRootaccept a URL as well as a path, so a value with a URI scheme gets no link.TypeScriptVersionManagerdecided which install is active inside its constructor. That rule moves into an exported function beside it, so thelibresolver can ask the same question instead of reading the manager's storage key and reimplementing the rule. The version provider is configured at activation from the two tsdk settings alone, and reconfigured when they change, soliblinks work with the native preview enabled, where no service client ever configures it. The full configuration load is not used for this because it shells out synchronously whentsserver.nodePathis set tonode, and that must not sit on the activation path.Deliberately left out
Each is a row plus at most a resolver once this lands:
compilerOptions.pathsvalues, which resolve againstbaseUrland so need document context the resolver contract does not carry. They would also need an object wildcard, sincepathskeys are globs.plugins[].nameandjsxImportSource, which are package resolution rather than path resolution.watchOptions.excludeFilesandexcludeDirectories.out.Three known limitations are recorded in comments:
typesentries are not resolved againsttypeRoots, the@typescript/lib-*override introduced in TypeScript 5.0 is not honored, and a link followed from its hover rather than from the document has its command arguments decoded once more than a ctrl+click does, so a value carrying a literal%resolves differently between the two gestures.Supersedes
This replaces two of my open pull requests, which I am closing in favor of it:
extends, which falls out of the contribution table rather than needing its own branch.liblinks, whose version lookup is done here through a shared helper rather than by exporting the version manager's storage key.It also touches the same seam as #318274, which makes the document selector configurable. That one is unaffected in substance: rebased onto this, it replaces only the selector-building step in
index.tsand stops touching link logic.tsconfig.tsbecomestsconfig/index.tshere, so whichever lands second needs a rebase.How to test
tsconfig.jsonsuch as:{ "extends": ["./tsconfig.base.json"], "files": ["src/main.ts"], "include": ["src/**/*"], "compilerOptions": { "lib": ["DOM", "ES2022"], "types": ["node"], "typeRoots": ["./typings"], "rootDir": "./src", "outDir": "./out" } }tsconfig.base.json,src/,typings/andnode_modules/@types/nodepresent, and noout/.DOMopenslib.dom.d.ts, andnodeopens the@types/nodedeclaration.Add
"ES7"and"ESNext.BigInt"toliband confirm they openlib.es2016.d.tsandlib.es2020.bigint.d.ts; add"NotALib"and confirm it reports that the lib could not be resolved."include": ["src/**/*"], confirm onlysrcis underlined and that following it reveals the folder.outDirbefore building and confirm it reports that the path does not exist yet.libentry again and confirm the target follows the selection.extendsstill works, and thatfilesandreferencesare unaffected.outDirat an existing directory outside the workspace and confirm following it reveals the folder in the OS file manager../scripts/code-web.sh <folder>, afternpm run bundle-webin the extension), followDOMand confirm it opens as a readonly editor rather than a browser tab.Unit tests cover the selection layer, the path classification, the lib map reading and the command handler's branches:
./scripts/test-integration.sh --suite typescript.🤖 Generated with Claude Code
https://claude.ai/code/session_014hRTv3iMuJ7GVzFybCs5j7
https://claude.ai/code/session_01WadC2xvsDdvGEyyV4ia2B8