mirror of
https://github.com/Microsoft/vscode
synced 2024-10-30 03:25:38 +00:00
Resolve correct link path for tsconfig.extends (#141062)
* fix(ts-features-extension): resolve correct path for `extends` of tsconfig fixes #131643 * always provide link. add command to resolve the link path on click * cleanup just make the code cleaner * revert `yarn.lock` changes * pretending eslint * use `vscode.open` * don't add `.json` to path if it's already here this change better conforms the TS resolving algorithm (see the reference) * style: move `resolveNodeModulesPath` to top level * don't show falsy errors on absolute paths * improve resolveNodeModulesPath impl - fixed a bug with infinite loop - check for module existence once per level
This commit is contained in:
parent
6e81cb0464
commit
c134702cc4
2 changed files with 95 additions and 14 deletions
|
@ -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",
|
||||
|
|
|
@ -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<R>(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R): R[] {
|
||||
return node && node.type === 'array' && node.children
|
||||
|
@ -14,6 +17,14 @@ function mapChildren<R>(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<vscode.Uri | undefined> => {
|
||||
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<vscode.Uri | undefined> => {
|
||||
// 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()),
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue