├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── copy-with-imports.gif ├── icon.png ├── package-lock.json ├── package.json ├── src ├── config.ts ├── extension.ts ├── imports.ts ├── pathresolver.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts ├── tsconfig.ts └── walk.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | dist 4 | .vscode-test -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | tabWidth: 4 3 | singleQuote: true 4 | quoteProps: "preserve" 5 | trailingComma: all 6 | bracketSpacing: false 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": [ "${workspaceRoot}/out/src/**/*.js" ], 14 | "preLaunchTask": "npm" 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" ], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outFiles": [ "${workspaceRoot}/out/test/**/*.js" ], 25 | "preLaunchTask": "npm" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | } 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "2.0.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // we run the custom script "compile" as defined in package.json 17 | "args": ["run", "compile", "--loglevel", "silent"], 18 | 19 | // The tsc compiler is started in watching mode 20 | "isBackground": true, 21 | 22 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 23 | "problemMatcher": "$tsc-watch", 24 | "tasks": [ 25 | { 26 | "label": "npm", 27 | "type": "shell", 28 | "command": "npm", 29 | "args": ["run", "compile", "--loglevel", "silent"], 30 | "isBackground": true, 31 | "problemMatcher": "$tsc-watch", 32 | "group": { 33 | "_id": "build", 34 | "isDefault": false 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | src/** 5 | .gitignore 6 | .yarnrc 7 | vsc-extension-quickstart.md 8 | **/tsconfig.json 9 | **/.eslintrc.json 10 | **/*.map 11 | **/*.ts 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the "copy-with-imports" extension will be documented in this file. 3 | 4 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 5 | 6 | ## [Unreleased] 7 | - Initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Features 2 | 3 | When copying and pasting code between files, this extension will attempt to add new imports the file you are pasting into. 4 | 5 | ![demo](copy-with-imports.gif) 6 | 7 | ## Known Issues 8 | 9 | ## Release Notes 10 | 11 | ### 1.0.0 12 | 13 | Update extension to work with vscode version 1.79.0+ 14 | 15 | ### 0.1.5 16 | 17 | Fix for projects that don't have a tsconfig.json. 18 | 19 | ### 0.1.3 20 | 21 | Add formatting preferences: `copy-with-imports.space-between-braces` and `copy-with-imports.double-quotes`. Defaults to false but can be set in user settings. 22 | 23 | ### 0.1.2 24 | 25 | Support es6 style imports when copying and pasting between JavaScript files 26 | 27 | ### 0.1.1 28 | 29 | Fix relative paths in Windows. 30 | 31 | ### 0.1.0 32 | 33 | Added the ability to use classic module resolution for relative imports. This will create new import statements relative to the project's tsconfig.json. 34 | 35 | To enable, update your user preferences to have: `"copy-with-imports.path-relative-from-tsconfig": true` 36 | 37 | ### 0.0.11 38 | 39 | Add imports to exisitng import statements if possible. 40 | 41 | ### 0.0.10 42 | 43 | Added support for exported functions. 44 | 45 | ### 0.0.9 46 | 47 | Fix bug with finding the position of the end of the last import. 48 | 49 | ### 0.0.8 50 | 51 | Add extension icon 52 | 53 | ### 0.0.7 54 | 55 | Add default key bindings for mac 56 | 57 | ### 0.0.5 58 | 59 | Add support for exported interfaces, enums, type aliases, and modules 60 | 61 | ### 0.0.4 62 | 63 | Auto import exported names from the file you copied from. 64 | 65 | ### 0.0.3 66 | 67 | Add a newline after the last inserted import statement. 68 | 69 | ### 0.0.2 70 | 71 | Added the ability to work with cut. 72 | 73 | Combine new imports from the same module into one statement. 74 | ### 0.0.1 75 | 76 | Initial release of Copy With Imports 77 | -------------------------------------------------------------------------------- /copy-with-imports.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringham/copy-with-imports/4463e9c668b6b0a73ff03eb7c8768e5b9d3a6bdf/copy-with-imports.gif -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringham/copy-with-imports/4463e9c668b6b0a73ff03eb7c8768e5b9d3a6bdf/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copy-with-imports", 3 | "displayName": "Copy With Imports", 4 | "description": "Copy over symbol imports when copying and pasting between TypeScript files.", 5 | "version": "1.0.2", 6 | "publisher": "stringham", 7 | "engines": { 8 | "vscode": "^1.79.0" 9 | }, 10 | "icon": "icon.png", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/stringham/copy-with-imports" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | "*" 20 | ], 21 | "keywords": [ 22 | "typescript", 23 | "import", 24 | "copy", 25 | "paste", 26 | "auto" 27 | ], 28 | "main": "./dist/extension.js", 29 | "contributes": { 30 | "commands": [ 31 | { 32 | "command": "copy-with-imports.copy", 33 | "title": "Copy with imports" 34 | }, 35 | { 36 | "command": "copy-with-imports.paste", 37 | "title": "Paste with imports" 38 | }, 39 | { 40 | "command": "copy-with-imports.cut", 41 | "title": "Cut with imports" 42 | } 43 | ], 44 | "configuration": { 45 | "type": "object", 46 | "title": "Copy With Imports configuration", 47 | "properties": { 48 | "copy-with-imports.path-relative-from-tsconfig": { 49 | "type": "boolean", 50 | "default": false, 51 | "description": "When resolving relative paths, use classic module resolution from the tsconfig. This makes the import relative from the tsconfig.json" 52 | }, 53 | "copy-with-imports.space-between-braces": { 54 | "type": "boolean", 55 | "default": false, 56 | "description": "Add spaces between the braces in generated import statements" 57 | }, 58 | "copy-with-imports.double-quotes": { 59 | "type": "boolean", 60 | "default": false, 61 | "description": "Use double quotes instead of single quotes for the module specifier string" 62 | } 63 | } 64 | }, 65 | "keybindings": [ 66 | { 67 | "command": "copy-with-imports.copy", 68 | "key": "ctrl+c", 69 | "mac": "cmd+c", 70 | "when": "editorTextFocus" 71 | }, 72 | { 73 | "command": "copy-with-imports.paste", 74 | "key": "ctrl+v", 75 | "mac": "cmd+v", 76 | "when": "editorTextFocus" 77 | }, 78 | { 79 | "command": "copy-with-imports.cut", 80 | "key": "ctrl+x", 81 | "mac": "cmd+x", 82 | "when": "editorTextFocus" 83 | } 84 | ] 85 | }, 86 | "scripts": { 87 | "vscode:prepublish": "npm run package", 88 | "compile": "webpack", 89 | "watch": "webpack --watch", 90 | "package": "webpack --mode production --devtool hidden-source-map", 91 | "test-compile": "tsc -p ./", 92 | "test-watch": "tsc -watch -p ./", 93 | "pretest": "npm run test-compile", 94 | "lint": "eslint src --ext ts", 95 | "test": "node ./out/test/runTest.js" 96 | }, 97 | "devDependencies": { 98 | "@types/vscode": "1.79.0", 99 | "@types/glob": "^8.1.0", 100 | "@types/mocha": "^10.0.1", 101 | "@types/node": "^20.2.5", 102 | "eslint": "^8.41.0", 103 | "@typescript-eslint/eslint-plugin": "^5.59.8", 104 | "@typescript-eslint/parser": "^5.59.8", 105 | "glob": "^7.1.6", 106 | "mocha": "^10.2.0", 107 | "typescript": "^5.1.3", 108 | "vscode-test": "^1.5.0", 109 | "ts-loader": "^8.0.14", 110 | "webpack": "^5.19.0", 111 | "webpack-cli": "^4.4.0", 112 | "source-map-support": "0.5.21" 113 | }, 114 | "dependencies": { 115 | "typescript": "^5.1.3" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function getExtensionConfig(property: string, defaultValue: T) { 4 | return vscode.workspace.getConfiguration('copy-with-imports').get(property, defaultValue); 5 | } 6 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import * as ts from 'typescript'; 5 | import {walk} from './walk'; 6 | import {getImports, ImportOptions} from './imports'; 7 | import {jsExtensions, tsExtensions, removeExtension, getRelativePath} from './pathresolver'; 8 | import {getExtensionConfig} from './config'; 9 | 10 | async function bringInImports(sourceFile: string, editor: vscode.TextEditor, text: string) { 11 | let sourceExt = path.extname(sourceFile); 12 | let destExt = path.extname(editor.document.fileName); 13 | let bothTs = tsExtensions.has(sourceExt) && tsExtensions.has(destExt); 14 | let bothJs = jsExtensions.has(sourceExt) && jsExtensions.has(destExt); 15 | if (!bothTs && !bothJs) { 16 | return; 17 | } 18 | 19 | let srcFileText = fs.readFileSync(sourceFile, 'utf8').toString(); 20 | const srcFileImports = getImports(srcFileText, sourceFile); 21 | 22 | let destinationFileText = editor.document.getText(); 23 | const destinationFileImports = getImports(destinationFileText, editor.document.fileName); 24 | 25 | const importsToAdd: {names: string[]; options: ImportOptions}[] = []; 26 | 27 | let file = ts.createSourceFile(editor.document.fileName, text, ts.ScriptTarget.Latest, true); 28 | const keep = new Set(); 29 | walk(file, (node) => { 30 | if (ts.isIdentifier(node)) { 31 | keep.add(node.getText()); 32 | } 33 | }); 34 | 35 | const destinationNameSpaceImports = new Map(); 36 | for (let importName in destinationFileImports) { 37 | let option = destinationFileImports[importName]; 38 | if (option.isImport && !(option.defaultImport || option.namespace) && option.moduleSpecifier) { 39 | if (!destinationNameSpaceImports.has(option.moduleSpecifier)) { 40 | destinationNameSpaceImports.set(option.moduleSpecifier, {names: [], option: option}); 41 | } 42 | let name = importName; 43 | if (option.originalName) { 44 | name = option.originalName + ' as ' + importName; 45 | } 46 | let existing = destinationNameSpaceImports.get(option.moduleSpecifier)!; 47 | if (option.end > existing.option.end) { 48 | existing.names = []; 49 | existing.option = option; 50 | } 51 | existing.names.push(name); 52 | } 53 | } 54 | 55 | for (let importName in srcFileImports) { 56 | if (destinationFileImports.hasOwnProperty(importName)) { 57 | continue; 58 | } 59 | if (keep.has(importName)) { 60 | let option = srcFileImports[importName]; 61 | if (option.defaultImport || option.namespace) { 62 | importsToAdd.push({names: [importName], options: srcFileImports[importName]}); 63 | } else { 64 | const name = (option.originalName ? option.originalName + ' as ' : '') + importName; 65 | let found = false; 66 | for (let i = 0; i < importsToAdd.length; i++) { 67 | if ( 68 | importsToAdd[i].options.path == option.path && 69 | !importsToAdd[i].options.defaultImport && 70 | !importsToAdd[i].options.namespace 71 | ) { 72 | importsToAdd[i].names.push(name); 73 | found = true; 74 | break; 75 | } 76 | } 77 | if (!found) { 78 | importsToAdd.push({names: [name], options: option}); 79 | } 80 | } 81 | } 82 | } 83 | 84 | if (importsToAdd.length > 0) { 85 | let lastImport: vscode.Position | null = null; 86 | let lastImportPos = 0; 87 | 88 | for (let importName in destinationFileImports) { 89 | let info = destinationFileImports[importName]; 90 | if (info.isImport && info.end > lastImportPos) { 91 | lastImportPos = info.end; 92 | lastImport = editor.document.positionAt(info.end); 93 | } 94 | } 95 | 96 | let importStatements: string[] = []; 97 | 98 | let replacements: {range: vscode.Range; value: string}[] = []; 99 | 100 | let spacesBetweenBraces = getExtensionConfig('space-between-braces', false); 101 | let doubleQuotes = getExtensionConfig('double-quotes', false); 102 | 103 | let optSpace = spacesBetweenBraces ? ' ' : ''; 104 | let quote = doubleQuotes ? '"' : "'"; 105 | 106 | importsToAdd.forEach((i) => { 107 | let statement = 'import '; 108 | let specifier = removeExtension(getRelativePath(editor.document.fileName, i.options.path)); 109 | if (i.options.namespace) { 110 | statement += '* as ' + i.names[0]; 111 | } else if (i.options.defaultImport) { 112 | statement += i.names[0]; 113 | } else { 114 | if (destinationNameSpaceImports.has(specifier)) { 115 | // add to existing imports 116 | const existing = destinationNameSpaceImports.get(specifier)!; 117 | const allNames = existing.names.concat(i.names); 118 | let range = new vscode.Range( 119 | editor.document.positionAt(existing.option.node.getStart()), 120 | editor.document.positionAt(existing.option.node.getEnd()), 121 | ); 122 | replacements.push({ 123 | range: range, 124 | value: `import {${optSpace}${allNames.join( 125 | ', ', 126 | )}${optSpace}} from ${quote}${specifier}${quote};`, 127 | }); 128 | return; 129 | } else { 130 | statement += `{${optSpace}${i.names.join(', ')}${optSpace}}`; 131 | } 132 | } 133 | 134 | statement += ` from ${quote}${specifier}${quote};`; 135 | importStatements.push(statement); 136 | }); 137 | 138 | // Workaround for edits sometimes failing to be applied on the first run. 139 | // It happens quite consistently when the start indentations are different. 140 | const MAX_RETRIES = 2; 141 | for (let i = 0; i < MAX_RETRIES; i++) { 142 | const editSuccessful = await editor.edit( 143 | (builder) => { 144 | if (importStatements.length > 0) { 145 | builder.insert( 146 | new vscode.Position(lastImport ? lastImport.line + 1 : 0, 0), 147 | importStatements.join('\n') + '\n', 148 | ); 149 | } 150 | replacements.forEach((r) => { 151 | builder.replace(r.range, r.value); 152 | }); 153 | }, 154 | {undoStopBefore: true, undoStopAfter: true}, 155 | ); 156 | if (editSuccessful) { 157 | break; 158 | } 159 | } 160 | } 161 | } 162 | 163 | 164 | export function activate(context: vscode.ExtensionContext) { 165 | let lastSave = { 166 | text: [''], 167 | location: '', 168 | }; 169 | 170 | function sortSelectionsByStartPosition(selections: readonly vscode.Selection[]) { 171 | return selections.slice().sort((a, b) => a.start.compareTo(b.start)); 172 | } 173 | 174 | function saveLastCopy(e: vscode.TextEditor) { 175 | const doc = e.document; 176 | const selections = sortSelectionsByStartPosition(e.selections); 177 | 178 | lastSave.text = selections.map((selection) => { 179 | let text = doc.getText(new vscode.Range(selection.start, selection.end)); 180 | if (text.length == 0) { 181 | // copying the whole line. 182 | text = doc.lineAt(selection.start.line).text + '\n'; 183 | } 184 | return text; 185 | }); 186 | 187 | lastSave.location = doc.fileName; 188 | } 189 | 190 | context.subscriptions.push( 191 | vscode.commands.registerCommand('copy-with-imports.copy', () => { 192 | vscode.commands.executeCommand('editor.action.clipboardCopyAction'); 193 | if (vscode.window.activeTextEditor) { 194 | saveLastCopy(vscode.window.activeTextEditor); 195 | } 196 | }), 197 | ); 198 | 199 | context.subscriptions.push( 200 | vscode.commands.registerCommand('copy-with-imports.cut', () => { 201 | if (vscode.window.activeTextEditor) { 202 | saveLastCopy(vscode.window.activeTextEditor); 203 | } 204 | vscode.commands.executeCommand('editor.action.clipboardCutAction'); 205 | }), 206 | ); 207 | 208 | context.subscriptions.push( 209 | vscode.commands.registerCommand('copy-with-imports.paste', async () => { 210 | let doc = vscode.window.activeTextEditor?.document; 211 | let selections = vscode.window.activeTextEditor 212 | ? sortSelectionsByStartPosition(vscode.window.activeTextEditor.selections) 213 | : undefined; 214 | 215 | const waitingForSelectionChange = new Promise((resolve) => { 216 | const registration = vscode.window.onDidChangeTextEditorSelection((e) => { 217 | registration.dispose(); 218 | resolve(); 219 | }); 220 | }); 221 | 222 | await vscode.commands.executeCommand('editor.action.clipboardPasteAction'); 223 | await waitingForSelectionChange; 224 | 225 | const activeEditor = vscode.window.activeTextEditor; 226 | if (!doc || !selections || !activeEditor) { 227 | return; 228 | } 229 | if (lastSave.location && activeEditor.document.fileName != lastSave.location) { 230 | let shouldBringImports = false; 231 | if (selections.length == 1 || selections.length != lastSave.text.length) { 232 | let selection = selections[0]; 233 | let pasted = doc.getText(new vscode.Range(selection.start, activeEditor.selection.start)); 234 | // replace whitespace in case of auto formatter. 235 | if (pasted.replace(/\s/g, '') == lastSave.text.join('').replace(/\s/g, '')) { 236 | shouldBringImports = true; 237 | } 238 | } else { 239 | let copied = new Set(lastSave.text.map((text) => text.replace(/\s/g, ''))); 240 | let currentSelections = sortSelectionsByStartPosition(activeEditor.selections); 241 | shouldBringImports = true; 242 | for (let i = 0; i < selections.length; i++) { 243 | let pasted = doc.getText(new vscode.Range(selections[i].start, currentSelections[i].start)); 244 | if (!copied.has(pasted.replace(/\s/g, ''))) { 245 | shouldBringImports = false; 246 | break; 247 | } 248 | } 249 | } 250 | if (shouldBringImports) { 251 | await bringInImports(lastSave.location, activeEditor, lastSave.text.join('\n')); 252 | } 253 | } 254 | }), 255 | ); 256 | } 257 | 258 | // this method is called when your extension is deactivated 259 | export function deactivate() {} 260 | -------------------------------------------------------------------------------- /src/imports.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as ts from 'typescript'; 3 | 4 | import {getTsConfig} from './tsconfig'; 5 | import {walk} from './walk'; 6 | 7 | export interface ImportOptions { 8 | path: string; 9 | isImport: boolean; 10 | end: number; 11 | node: ts.Node; 12 | moduleSpecifier?: string; 13 | namespace?: boolean; 14 | defaultImport?: boolean; 15 | originalName?: string; 16 | } 17 | 18 | export function getImports(src: string, filePath: string) { 19 | let file = ts.createSourceFile(filePath, src, ts.ScriptTarget.Latest, true); 20 | 21 | let importNames: {[key: string]: ImportOptions} = {}; 22 | const config = getTsConfig(filePath); 23 | for (const node of file.statements) { 24 | if (ts.isImportDeclaration(node)) { 25 | if (ts.isStringLiteral(node.moduleSpecifier)) { 26 | let specifier = node.moduleSpecifier.text; 27 | let resolved = resolveImport(specifier, filePath, config); 28 | 29 | if (node.importClause) { 30 | if (node.importClause.name) { 31 | importNames[node.importClause.name.getText()] = { 32 | path: resolved, 33 | defaultImport: true, 34 | isImport: true, 35 | end: node.getEnd(), 36 | node: node, 37 | }; 38 | } 39 | if (node.importClause.namedBindings) { 40 | let bindings = node.importClause.namedBindings; 41 | if (ts.isNamedImports(bindings)) { 42 | bindings.elements.forEach((a) => { 43 | importNames[a.name.getText()] = { 44 | path: resolved, 45 | originalName: a.propertyName ? a.propertyName.getText() : undefined, 46 | moduleSpecifier: (node.moduleSpecifier as ts.StringLiteral).text, 47 | isImport: true, 48 | end: node.getEnd(), 49 | node: node, 50 | }; 51 | }); 52 | } else if (ts.isNamespaceImport(bindings)) { 53 | importNames[bindings.name.getText()] = { 54 | path: resolved, 55 | namespace: true, 56 | isImport: true, 57 | end: node.getEnd(), 58 | node: node, 59 | }; 60 | } else { 61 | console.log('unexpected..'); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | if (ts.isExportDeclaration(node)) { 69 | if (node.exportClause && ts.isNamedExports(node.exportClause)) { 70 | for (const exportSpecifier of node.exportClause.elements) { 71 | importNames[exportSpecifier.name.getText()] = { 72 | path: filePath, 73 | isImport: false, 74 | end: -1, 75 | node: exportSpecifier.name, 76 | }; 77 | } 78 | } 79 | } 80 | 81 | if ( 82 | ts.isClassDeclaration(node) || 83 | ts.isVariableStatement(node) || 84 | ts.isInterfaceDeclaration(node) || 85 | ts.isEnumDeclaration(node) || 86 | ts.isTypeAliasDeclaration(node) || 87 | ts.isModuleDeclaration(node) || 88 | ts.isFunctionDeclaration(node) 89 | ) { 90 | const isExported = (node.modifiers ?? []).some((m) => m.kind == ts.SyntaxKind.ExportKeyword); 91 | if (isExported) { 92 | if (ts.isClassDeclaration(node)) { 93 | if (node.name) { 94 | importNames[node.name.getText()] = { 95 | path: filePath, 96 | isImport: false, 97 | end: -1, 98 | node: node, 99 | }; 100 | } 101 | } else if (ts.isVariableStatement(node)) { 102 | node.declarationList.declarations.forEach((declaration) => { 103 | importNames[declaration.name.getText()] = { 104 | path: filePath, 105 | isImport: false, 106 | end: -1, 107 | node: node, 108 | }; 109 | }); 110 | } else if (ts.isInterfaceDeclaration(node)) { 111 | importNames[node.name.getText()] = { 112 | path: filePath, 113 | isImport: false, 114 | end: -1, 115 | node: node, 116 | }; 117 | } else if (ts.isEnumDeclaration(node)) { 118 | importNames[node.name.getText()] = { 119 | path: filePath, 120 | isImport: false, 121 | end: -1, 122 | node: node, 123 | }; 124 | } else if (ts.isTypeAliasDeclaration(node)) { 125 | importNames[node.name.getText()] = { 126 | path: filePath, 127 | isImport: false, 128 | end: -1, 129 | node: node, 130 | }; 131 | } else if (ts.isModuleDeclaration(node)) { 132 | importNames[node.name.getText()] = { 133 | path: filePath, 134 | isImport: false, 135 | end: -1, 136 | node: node, 137 | }; 138 | } else if (ts.isFunctionDeclaration(node)) { 139 | if (node.name) { 140 | importNames[node.name.getText()] = { 141 | path: filePath, 142 | isImport: false, 143 | end: -1, 144 | node: node, 145 | }; 146 | } 147 | } 148 | } 149 | } 150 | } 151 | return importNames; 152 | } 153 | 154 | export function resolveImport(importSpecifier: string, filePath: string, config: any): string { 155 | if (importSpecifier.startsWith('.')) { 156 | return path.resolve(path.dirname(filePath), importSpecifier) + '.ts'; 157 | } 158 | if (config && config.config.compilerOptions && config.config.compilerOptions.paths) { 159 | for (let p in config.config.compilerOptions.paths) { 160 | if (p.endsWith('*') && importSpecifier.startsWith(p.replace('*', ''))) { 161 | if (config.config.compilerOptions.paths[p].length == 1) { 162 | let mapped = config.config.compilerOptions.paths[p][0].replace('*', ''); 163 | let mappedDir = path.resolve(path.dirname(config.path), mapped); 164 | return mappedDir + '/' + importSpecifier.substr(p.replace('*', '').length) + '.ts'; 165 | } 166 | } 167 | } 168 | } 169 | return importSpecifier; 170 | } 171 | -------------------------------------------------------------------------------- /src/pathresolver.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import {getExtensionConfig} from './config'; 4 | import {getTsConfig} from './tsconfig'; 5 | 6 | export const tsExtensions = new Set(['.ts', '.tsx', '.mts', '.cts', '.mtsx', '.ctsx']); 7 | export const jsExtensions = new Set(['.js', '.jsx', '.mjs', '.cjs', '.mjsx', '.cjsx']); 8 | 9 | function isPathToAnotherDir(path: string) { 10 | return path.startsWith('../') || path.startsWith('..\\'); 11 | } 12 | 13 | function isInDir(dir: string, p: string) { 14 | let relative = path.relative(dir, p); 15 | return !isPathToAnotherDir(relative); 16 | } 17 | 18 | export function removeExtension(filePath: string): string { 19 | let ext = path.extname(filePath); 20 | let extensions = new Set([...tsExtensions, ...jsExtensions]); 21 | if (ext == '.ts' && filePath.endsWith('.d.ts')) { 22 | ext = '.d.ts'; 23 | } 24 | if (extensions.has(ext)) { 25 | return filePath.slice(0, -ext.length); 26 | } 27 | return filePath; 28 | } 29 | 30 | function convertPathSeperators(relative: string) { 31 | return relative.replace(/\\/g, '/'); 32 | } 33 | 34 | export function getRelativePath(fromPath: string, specifier: string): string { 35 | if (tsExtensions.has(path.extname(fromPath))) { 36 | const config = getTsConfig(fromPath); 37 | if (config && config.config && config.config.compilerOptions && config.config.compilerOptions.paths) { 38 | for (let p in config.config.compilerOptions.paths) { 39 | if (config.config.compilerOptions.paths[p].length == 1) { 40 | let mapped = config.config.compilerOptions.paths[p][0].replace('*', ''); 41 | let mappedDir = path.resolve(path.dirname(config.path), mapped); 42 | if (isInDir(mappedDir, specifier)) { 43 | return convertPathSeperators(p.replace('*', '') + path.relative(mappedDir, specifier)); 44 | } 45 | } 46 | } 47 | } 48 | if ( 49 | config && 50 | config.config && 51 | isInDir(path.dirname(config.path), specifier) && 52 | getExtensionConfig('path-relative-from-tsconfig', false) 53 | ) { 54 | return convertPathSeperators(path.relative(path.dirname(config.path), specifier)); 55 | } 56 | } 57 | 58 | if (!path.isAbsolute(specifier)) { 59 | return convertPathSeperators(specifier); 60 | } 61 | 62 | let relative = path.relative(path.dirname(fromPath), specifier); 63 | if (!relative.startsWith('.')) { 64 | relative = './' + relative; 65 | } 66 | return convertPathSeperators(relative); 67 | } 68 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from 'vscode-test'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | import {getImports} from '../../imports'; 7 | // import * as myExtension from '../../extension'; 8 | 9 | suite('Extension Test Suite', () => { 10 | vscode.window.showInformationMessage('Start all tests.'); 11 | 12 | test('Sample test', () => { 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 14 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 15 | }); 16 | 17 | test('imports', () => { 18 | vscode.window.showInformationMessage('Start imports tests.'); 19 | 20 | const fileContents = `import {foo} from './foo'; 21 | import {baz as bar} from './baz'; 22 | import {one, two, three as four} from './numbers'; 23 | import * as namespaceImport from './namespace'; 24 | import defaultImport from './defaultImport'; 25 | import nonRelative from '@this/is/not/relative'; 26 | 27 | 28 | export {ReExport} from 'otherfile'; 29 | export class MyClass {} 30 | 31 | export const myConst = 4; 32 | export let myLet = 3; 33 | export var myVar = 2; 34 | export interface myInterface {} 35 | export enum myEnum {} 36 | export type myType = 4|3|2|1; 37 | export function myFunction() {} 38 | export namespace myModule {} 39 | `; 40 | const imports = getImports(fileContents, '/this/is/a/file.ts'); 41 | 42 | console.log(imports, imports); 43 | 44 | const expectation: Record = { 45 | bar: '/this/is/a/baz.ts', 46 | defaultImport: '/this/is/a/defaultImport.ts', 47 | foo: '/this/is/a/foo.ts', 48 | four: '/this/is/a/numbers.ts', 49 | MyClass: '/this/is/a/file.ts', 50 | myConst: '/this/is/a/file.ts', 51 | myEnum: '/this/is/a/file.ts', 52 | myFunction: '/this/is/a/file.ts', 53 | myModule: '/this/is/a/file.ts', 54 | ReExport: '/this/is/a/file.ts', 55 | myInterface: '/this/is/a/file.ts', 56 | myLet: '/this/is/a/file.ts', 57 | myType: '/this/is/a/file.ts', 58 | myVar: '/this/is/a/file.ts', 59 | namespaceImport: '/this/is/a/namespace.ts', 60 | nonRelative: '@this/is/not/relative', 61 | one: '/this/is/a/numbers.ts', 62 | two: '/this/is/a/numbers.ts', 63 | }; 64 | 65 | for(const key in expectation) { 66 | assert.strictEqual(key in imports, true, key + ' not found'); 67 | assert.strictEqual(imports[key].path, expectation[key]); 68 | } 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/tsconfig.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as ts from 'typescript'; 4 | 5 | export function getTsConfig(filePath: string) { 6 | let dir = path.dirname(filePath); 7 | let lastDir = filePath; 8 | while (dir != lastDir) { 9 | const tsConfigPaths = [dir + '/tsconfig.build.json', dir + '/tsconfig.json']; 10 | const tsConfigPath = tsConfigPaths.find((p) => fs.existsSync(p)); 11 | if (tsConfigPath) { 12 | const config: any = ts.parseConfigFileTextToJson(tsConfigPath, fs.readFileSync(tsConfigPath).toString()); 13 | config.path = tsConfigPath; 14 | return config; 15 | } 16 | lastDir = dir; 17 | dir = path.dirname(dir); 18 | } 19 | return false; 20 | } 21 | -------------------------------------------------------------------------------- /src/walk.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export function walk(node: ts.Node, fn: (node: ts.Node) => any): boolean { 4 | if (fn(node)) { 5 | return true; 6 | } 7 | const children = node.getChildren(); 8 | for (let i = 0; i < children.length; i++) { 9 | if (walk(children[i], fn)) { 10 | return true; 11 | } 12 | } 13 | return false; 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | ".vscode-test" 16 | ] 17 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 11 | 12 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 13 | output: { 14 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: 'extension.js', 17 | libraryTarget: 'commonjs2' 18 | }, 19 | devtool: 'nosources-source-map', 20 | externals: { 21 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'] 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: 'ts-loader' 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | }; 41 | module.exports = config; --------------------------------------------------------------------------------