From 97ef3ff276a34dd16bec21e75c246aec5bb0741e Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 30 Aug 2024 14:37:52 +0200 Subject: [PATCH] first cut of import helper extension (#227203) --- .../.vscode/launch.json | 16 ++ .../.vscode/settings.json | 7 + .../vscode-selfhost-import-aid/package.json | 29 +++ .../src/extension.ts | 177 ++++++++++++++++++ .../vscode-selfhost-import-aid/tsconfig.json | 14 ++ .../vscode-selfhost-import-aid/yarn.lock | 60 ++++++ build/gulpfile.extensions.js | 1 + 7 files changed, 304 insertions(+) create mode 100644 .vscode/extensions/vscode-selfhost-import-aid/.vscode/launch.json create mode 100644 .vscode/extensions/vscode-selfhost-import-aid/.vscode/settings.json create mode 100644 .vscode/extensions/vscode-selfhost-import-aid/package.json create mode 100644 .vscode/extensions/vscode-selfhost-import-aid/src/extension.ts create mode 100644 .vscode/extensions/vscode-selfhost-import-aid/tsconfig.json create mode 100644 .vscode/extensions/vscode-selfhost-import-aid/yarn.lock diff --git a/.vscode/extensions/vscode-selfhost-import-aid/.vscode/launch.json b/.vscode/extensions/vscode-selfhost-import-aid/.vscode/launch.json new file mode 100644 index 00000000000..50b8aedaf72 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-import-aid/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--enable-proposed-api=ms-vscode.vscode-selfhost-import-aid" + ], + "name": "Launch Extension", + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "request": "launch", + "type": "extensionHost" + } + ] +} diff --git a/.vscode/extensions/vscode-selfhost-import-aid/.vscode/settings.json b/.vscode/extensions/vscode-selfhost-import-aid/.vscode/settings.json new file mode 100644 index 00000000000..e4429caeee4 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-import-aid/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } +} diff --git a/.vscode/extensions/vscode-selfhost-import-aid/package.json b/.vscode/extensions/vscode-selfhost-import-aid/package.json new file mode 100644 index 00000000000..30f09f9c8e5 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-import-aid/package.json @@ -0,0 +1,29 @@ +{ + "name": "vscode-selfhost-import-aid", + "displayName": "VS Code Selfhost Import Aid", + "description": "Util to improve dealing with imports", + "engines": { + "vscode": "^1.88.0" + }, + "version": "0.0.1", + "publisher": "ms-vscode", + "categories": [ + "Other" + ], + "activationEvents": [ + "onLanguage:typescript" + ], + "main": "./out/extension.js", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + }, + "license": "MIT", + "scripts": { + "compile": "gulp compile-extension:vscode-selfhost-import-aid", + "watch": "gulp watch-extension:vscode-selfhost-import-aid" + }, + "dependencies": { + "typescript": "5.5.4" + } +} diff --git a/.vscode/extensions/vscode-selfhost-import-aid/src/extension.ts b/.vscode/extensions/vscode-selfhost-import-aid/src/extension.ts new file mode 100644 index 00000000000..120d1a15351 --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-import-aid/src/extension.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as ts from 'typescript'; +import * as path from 'path'; + +export async function activate(context: vscode.ExtensionContext) { + + const fileIndex = new class { + + private _currentRun?: Thenable; + + private _disposables: vscode.Disposable[] = []; + + private readonly _index = new Map(); + + constructor() { + const watcher = vscode.workspace.createFileSystemWatcher('**/*.ts', false, true, false); + this._disposables.push(watcher.onDidChange(e => { this._index.set(e.toString(), e); })); + this._disposables.push(watcher.onDidDelete(e => { this._index.delete(e.toString()); })); + this._disposables.push(watcher); + + this._refresh(false); + } + + dispose(): void { + for (const disposable of this._disposables) { + disposable.dispose(); + } + this._disposables = []; + this._index.clear(); + } + + async all() { + await this._currentRun; + return this._index.values(); + } + + private _refresh(clear: boolean) { + // TODO@jrieken LATEST API! findFiles2New + this._currentRun = vscode.workspace.findFiles('src/vs/**/*.ts', '{**/node_modules/**,**/extensions/**}').then(all => { + if (clear) { + this._index.clear(); + } + for (const item of all) { + this._index.set(item.toString(), item); + } + }); + } + }; + + const selector: vscode.DocumentSelector = 'typescript'; + + function findNodeAtPosition(document: vscode.TextDocument, node: ts.Node, position: vscode.Position): ts.Node | undefined { + if (node.getStart() <= document.offsetAt(position) && node.getEnd() >= document.offsetAt(position)) { + return ts.forEachChild(node, child => findNodeAtPosition(document, child, position)) || node; + } + return undefined; + } + + function findImportAt(document: vscode.TextDocument, position: vscode.Position): ts.ImportDeclaration | undefined { + const sourceFile = ts.createSourceFile(document.fileName, document.getText(), ts.ScriptTarget.Latest, true); + const node = findNodeAtPosition(document, sourceFile, position); + if (node && ts.isStringLiteral(node) && ts.isImportDeclaration(node.parent)) { + return node.parent; + } + return undefined; + } + + const completionProvider = new class implements vscode.CompletionItemProvider { + async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { + + const index = document.getText().lastIndexOf(' from \''); + if (index < 0 || document.positionAt(index).line < position.line) { + // line after last import is before position + // -> no completion, safe a parse call + return undefined; + } + + const node = findImportAt(document, position); + if (!node) { + return undefined; + } + + const range = new vscode.Range(document.positionAt(node.moduleSpecifier.pos), document.positionAt(node.moduleSpecifier.end)); + const uris = await Promise.race([fileIndex.all(), new Promise(resolve => token.onCancellationRequested(resolve))]); + + if (!uris) { + console.log('NO uris'); + return undefined; + } + + const result = new vscode.CompletionList(); + result.isIncomplete = true; + + for (const item of uris) { + + if (!item.path.endsWith('.ts')) { + continue; + } + + let relativePath = path.relative(path.dirname(document.uri.path), item.path); + relativePath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`; + + const label = path.basename(item.path, path.extname(item.path)); + const insertText = ` '${relativePath.replace(/\.ts$/, '.js')}'`; + const filterText = ` '${label}'`; + + const completion = new vscode.CompletionItem({ + label: label, + description: vscode.workspace.asRelativePath(item), + }); + completion.kind = vscode.CompletionItemKind.File; + completion.insertText = insertText; + completion.filterText = filterText; + completion.range = range; + + result.items.push(completion); + } + + return result; + } + }; + + class ImportCodeActions implements vscode.CodeActionProvider { + + static kind = vscode.CodeActionKind.QuickFix.append('import'); + + async provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): Promise { + + const diag = context.diagnostics.find(d => d.code === 2307); + if (!diag) { + return undefined; + } + + const node = findImportAt(document, range.start)?.moduleSpecifier; + + if (!node || !ts.isStringLiteral(node)) { + return undefined; + } + + const nodeRange = new vscode.Range(document.positionAt(node.pos), document.positionAt(node.end)); + const name = path.basename(node.text, path.extname(node.text)); + + const uris = await Promise.race([fileIndex.all(), new Promise(resolve => token.onCancellationRequested(resolve))]); + + if (!uris) { + console.log('NO uris'); + return []; + } + + const result: vscode.CodeAction[] = []; + + for (const item of uris) { + if (path.basename(item.path, path.extname(item.path)) === name) { + let relativePath = path.relative(path.dirname(document.uri.path), item.path).replace(/\.ts$/, '.js'); + relativePath = relativePath.startsWith('.') ? relativePath : `./${relativePath}`; + + const action = new vscode.CodeAction(`Fix to '${relativePath}'`, ImportCodeActions.kind); + action.edit = new vscode.WorkspaceEdit(); + action.edit.replace(document.uri, nodeRange, ` '${relativePath}'`); + action.diagnostics = [diag]; + result.push(action); + } + } + + return result; + } + } + + context.subscriptions.push(fileIndex); + context.subscriptions.push(vscode.languages.registerCompletionItemProvider(selector, completionProvider)); + context.subscriptions.push(vscode.languages.registerCodeActionsProvider(selector, new ImportCodeActions(), { providedCodeActionKinds: [ImportCodeActions.kind] })); +} diff --git a/.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json b/.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json new file mode 100644 index 00000000000..bbca76708ca --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../extensions/tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "types": [ + "node", + "mocha", + ] + }, + "include": [ + "src/**/*", + "../../../src/vscode-dts/vscode.d.ts" + ] +} diff --git a/.vscode/extensions/vscode-selfhost-import-aid/yarn.lock b/.vscode/extensions/vscode-selfhost-import-aid/yarn.lock new file mode 100644 index 00000000000..5dd9568bc9c --- /dev/null +++ b/.vscode/extensions/vscode-selfhost-import-aid/yarn.lock @@ -0,0 +1,60 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@types/istanbul-lib-coverage@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/mocha@^10.0.6": + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.6.tgz#818551d39113081048bdddbef96701b4e8bb9d1b" + integrity sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg== + +"@types/node@20.x": + version "20.12.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.11.tgz#c4ef00d3507000d17690643278a60dc55a9dc9be" + integrity sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw== + dependencies: + undici-types "~5.26.4" + +ansi-styles@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +cockatiel@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/cockatiel/-/cockatiel-3.1.3.tgz#bb1774a498a17e739dd994d56610dc6538b02858" + integrity sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ== + +istanbul-to-vscode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/istanbul-to-vscode/-/istanbul-to-vscode-2.0.1.tgz#84994d06e604b68cac7301840f338b1e74eb888b" + integrity sha512-V9Hhr7kX3UvkvkaT1lK3AmCRPkaIAIogQBrduTpNiLTkp1eVsybnJhWiDSVeCQap/3aGeZ2019oIivhX9MNsCQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.6" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 4631b295ae4..f00fa7a1ac3 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -68,6 +68,7 @@ const compilations = [ 'extensions/vscode-test-resolver/tsconfig.json', '.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json', + '.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json', ]; const getBaseUrl = out => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`;