diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 06555fdfa8da2..d97dade3ddd0f 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -2839,6 +2839,20 @@ "description": "%typescript.tsserver.pluginPaths%", "markdownDeprecationMessage": "%typescript.tsserver.pluginPaths.unifiedDeprecationMessage%", "scope": "machine" + }, + "js/ts.projectConfig.additionalFilePatterns": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "default": [], + "markdownDescription": "%js/ts.projectConfig.additionalFilePatterns%", + "scope": "window", + "keywords": [ + "TypeScript", + "JavaScript" + ] } } } diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index a697f8c00f277..f8fada2672253 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -35,6 +35,7 @@ "typescript.tsserver.pluginPaths": "Additional paths to discover TypeScript Language Service plugins.", "typescript.tsserver.pluginPaths.item": "Either an absolute or relative path. Relative path will be resolved against workspace folder(s).", "typescript.tsserver.pluginPaths.unifiedDeprecationMessage": "This setting is deprecated. Use `#js/ts.tsserver.pluginPaths#` instead.", + "js/ts.projectConfig.additionalFilePatterns": "Additional glob patterns identifying files that should receive `tsconfig.json`/`jsconfig.json`-style document links (for `extends`, `references`, and `files`). Useful for monorepos that name configs without `tsconfig`/`jsconfig` in the filename (for example `base.json`, `build.json`). The standard `tsconfig.json`/`jsconfig.json` patterns are always included; entries here are added to them. Does not affect TypeScript/JavaScript project loading.", "typescript.tsserver.trace": "Enables tracing of messages sent to the TS server. This trace can be used to diagnose TS Server issues. The trace may contain file paths, source code, and other potentially sensitive information from your project.", "typescript.validate.enable": "Enable/disable TypeScript validation.", "javascript.validate.enable": "Enable/disable JavaScript validation.", diff --git a/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts b/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts index 85fe3f6de5fe7..109daf9d0d077 100644 --- a/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts +++ b/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts @@ -118,6 +118,79 @@ class TsconfigLinkProvider implements vscode.DocumentLinkProvider { } } +class TsconfigLinkProviderRegistration implements vscode.Disposable { + + private static readonly configSection = 'js/ts'; + private static readonly configKey = 'projectConfig.additionalFilePatterns'; + private static readonly languages: readonly string[] = ['json', 'jsonc']; + private static readonly defaultPatterns: readonly string[] = [ + '**/[jt]sconfig.json', + '**/[jt]sconfig.*.json', + ]; + + private readonly _configListener: vscode.Disposable; + private _providerRegistration: vscode.Disposable | undefined; + private _lastPatternsKey: string | undefined; + + constructor(private readonly _provider: vscode.DocumentLinkProvider) { + this.update(); + this._configListener = vscode.workspace.onDidChangeConfiguration(e => { + const fullKey = `${TsconfigLinkProviderRegistration.configSection}.${TsconfigLinkProviderRegistration.configKey}`; + if (e.affectsConfiguration(fullKey)) { + this.update(); + } + }); + } + + dispose(): void { + this._configListener.dispose(); + this._providerRegistration?.dispose(); + this._providerRegistration = undefined; + } + + private update(): void { + const patterns = this.readPatterns(); + const key = patterns.join('\n'); + + if (key === this._lastPatternsKey) { + return; + } + + this._lastPatternsKey = key; + this._providerRegistration?.dispose(); + this._providerRegistration = vscode.languages.registerDocumentLinkProvider( + this.buildSelector(patterns), this._provider); + } + + private buildSelector(patterns: readonly string[]): vscode.DocumentSelector { + return TsconfigLinkProviderRegistration.languages.flatMap( + language => patterns.map((pattern): vscode.DocumentFilter => ({ language, pattern }))); + } + + private readPatterns(): string[] { + const patterns = new Set(TsconfigLinkProviderRegistration.defaultPatterns); + const configured = vscode.workspace + .getConfiguration(TsconfigLinkProviderRegistration.configSection) + .get(TsconfigLinkProviderRegistration.configKey); + + if (Array.isArray(configured)) { + for (const value of configured) { + if (typeof value !== 'string') { + continue; + } + + const trimmed = value.trim(); + + if (trimmed.length > 0) { + patterns.add(trimmed); + } + } + } + + return Array.from(patterns); + } +} + async function resolveNodeModulesPath(baseDirUri: vscode.Uri, pathCandidates: string[]): Promise { let currentUri = baseDirUri; const baseCandidate = pathCandidates[0]; @@ -191,17 +264,6 @@ async function getTsconfigPath(baseDirUri: vscode.Uri, pathValue: string, linkTy } export function register() { - const patterns: vscode.GlobPattern[] = [ - '**/[jt]sconfig.json', - '**/[jt]sconfig.*.json', - ]; - - const languages = ['json', 'jsonc']; - - const selector: vscode.DocumentSelector = - languages.map(language => patterns.map((pattern): vscode.DocumentFilter => ({ language, pattern }))) - .flat(); - return vscode.Disposable.from( vscode.commands.registerCommand(openExtendsLinkCommandId, async ({ resourceUri, extendsValue, linkType }: OpenExtendsLinkCommandArgs) => { const tsconfigPath = await getTsconfigPath(Utils.dirname(vscode.Uri.from(resourceUri)), extendsValue, linkType); @@ -212,6 +274,7 @@ export function register() { // Will suggest to create a .json variant if it doesn't exist yet (but only for relative paths) await vscode.commands.executeCommand('vscode.open', tsconfigPath); }), - vscode.languages.registerDocumentLinkProvider(selector, new TsconfigLinkProvider()), + new TsconfigLinkProviderRegistration(new TsconfigLinkProvider()), ); } +