diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index db64686da89..d79c8e9a8c1 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -37,10 +37,11 @@ "Programming Languages" ], "dependencies": { + "@vscode/extension-telemetry": "0.4.6", "jsonc-parser": "^2.2.1", "semver": "5.5.1", - "@vscode/extension-telemetry": "0.4.6", - "vscode-nls": "^5.0.0" + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.3" }, "devDependencies": { "@types/node": "14.x", diff --git a/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts b/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts index a181276f36e..56668f9fb31 100644 --- a/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts +++ b/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts @@ -4,9 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as jsonc from 'jsonc-parser'; -import { basename, dirname, join } from 'path'; +import * as nls from 'vscode-nls'; import * as vscode from 'vscode'; +import { basename, dirname, join, posix } from 'path'; import { coalesce, flatten } from '../utils/arrays'; +import { exists } from '../utils/fs'; +import { Utils } from 'vscode-uri'; function mapChildren(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R): R[] { return node && node.type === 'array' && node.children @@ -14,6 +17,14 @@ function mapChildren(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R): : []; } +const openExtendsLinkCommandId = '_typescript.openExtendsLink'; +type OpenExtendsLinkCommandArgs = { + resourceUri: vscode.Uri + extendsValue: string +}; + +const localize = nls.loadMessageBundle(); + class TsconfigLinkProvider implements vscode.DocumentLinkProvider { public provideDocumentLinks( @@ -38,21 +49,18 @@ class TsconfigLinkProvider implements vscode.DocumentLinkProvider { return undefined; } - if (extendsNode.value.startsWith('.')) { - return new vscode.DocumentLink( - this.getRange(document, extendsNode), - vscode.Uri.file(join(dirname(document.uri.fsPath), extendsNode.value + (extendsNode.value.endsWith('.json') ? '' : '.json'))) - ); - } - - const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri); - if (!workspaceFolder) { + const extendsValue: string = extendsNode.value; + if (extendsValue.startsWith('/')) { return undefined; } + const args: OpenExtendsLinkCommandArgs = { + resourceUri: document.uri, + extendsValue: extendsValue + }; return new vscode.DocumentLink( this.getRange(document, extendsNode), - vscode.Uri.joinPath(workspaceFolder.uri, 'node_modules', extendsNode.value + (extendsNode.value.endsWith('.json') ? '' : '.json')) + vscode.Uri.parse(`command:${openExtendsLinkCommandId}?${JSON.stringify(args)}`) ); } @@ -110,6 +118,67 @@ class TsconfigLinkProvider implements vscode.DocumentLinkProvider { } } +const resolveNodeModulesPath = async (baseDirUri: vscode.Uri, pathCandidates: string[]): Promise => { + let currentUri = baseDirUri; + const baseCandidate = pathCandidates[0]; + const sepIndex = baseCandidate.startsWith('@') ? 2 : 1; + const moduleBasePath = baseCandidate.split(posix.sep).slice(0, sepIndex).join(posix.sep); + while (true) { + const moduleAbsoluteUrl = vscode.Uri.joinPath(currentUri, 'node_modules', moduleBasePath); + let moduleStat: vscode.FileStat | undefined; + try { + moduleStat = await vscode.workspace.fs.stat(moduleAbsoluteUrl); + } catch (err) { } + if (moduleStat && (moduleStat.type & vscode.FileType.Directory)) { + for (const uriCandidate of pathCandidates + .map((relativePath) => relativePath.split(posix.sep).slice(sepIndex).join(posix.sep)) + // skip empty paths within module + .filter(Boolean) + .map((relativeModulePath) => vscode.Uri.joinPath(moduleAbsoluteUrl, relativeModulePath)) + ) { + if (await exists(uriCandidate)) { + return uriCandidate; + } + } + // Continue to loocking for potentially another version + } + + const oldUri = currentUri; + currentUri = vscode.Uri.joinPath(currentUri, '..'); + // Can't go next. Reached the system root + if (oldUri.path === currentUri.path) { + return; + } + } +}; + +// Reference: https://github.com/microsoft/TypeScript/blob/febfd442cdba343771f478cf433b0892f213ad2f/src/compiler/commandLineParser.ts#L3005 +/** +* @returns Returns undefined in case of lack of result while trying to resolve from node_modules +*/ +const getTsconfigPath = async (baseDirUri: vscode.Uri, extendsValue: string): Promise => { + // Don't take into account a case, where tsconfig might be resolved from the root (see the reference) + // e.g. C:/projects/shared-tsconfig/tsconfig.json (note that C: prefix is optional) + + const isRelativePath = ['./', '../'].some(str => extendsValue.startsWith(str)); + if (isRelativePath) { + const absolutePath = vscode.Uri.joinPath(baseDirUri, extendsValue); + if (await exists(absolutePath) || absolutePath.path.endsWith('.json')) { + return absolutePath; + } + return absolutePath.with({ + path: `${absolutePath.path}.json` + }); + } + + // Otherwise resolve like a module + return resolveNodeModulesPath(baseDirUri, [ + extendsValue, + ...extendsValue.endsWith('.json') ? [] : [`${extendsValue}.json`], + `${extendsValue}/tsconfig.json`, + ]); +}; + export function register() { const patterns: vscode.GlobPattern[] = [ '**/[jt]sconfig.json', @@ -122,5 +191,16 @@ export function register() { languages.map(language => patterns.map((pattern): vscode.DocumentFilter => ({ language, pattern })))); - return vscode.languages.registerDocumentLinkProvider(selector, new TsconfigLinkProvider()); + return vscode.Disposable.from( + vscode.commands.registerCommand(openExtendsLinkCommandId, async ({ resourceUri, extendsValue, }: OpenExtendsLinkCommandArgs) => { + const tsconfigPath = await getTsconfigPath(Utils.dirname(resourceUri), extendsValue); + if (tsconfigPath === undefined) { + vscode.window.showErrorMessage(localize('openTsconfigExtendsModuleFail', "Failed to resolve {0} as module", extendsValue)); + return; + } + // 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()), + ); }