├── .editorconfig ├── .gitignore ├── .gitlab-ci.yml ├── .vscode ├── launch.json └── settings.json ├── .vscodeignore ├── LICENSE ├── README.md ├── config ├── tsconfig.base.json ├── tsconfig.build.json └── tsconfig.test.json ├── etc ├── icon.png └── test_install.js ├── package-lock.json ├── package.json ├── src ├── activatable.ts ├── configuration │ ├── imports-config.ts │ └── index.ts ├── extension.ts ├── imports │ ├── import-grouping │ │ ├── import-group-identifier-invalid-error.ts │ │ ├── import-group-keyword.ts │ │ ├── import-group-order.ts │ │ ├── import-group-setting-parser.ts │ │ ├── import-group.ts │ │ ├── index.ts │ │ ├── keyword-import-group.ts │ │ ├── regex-import-group.ts │ │ └── remain-import-group.ts │ ├── import-manager.ts │ ├── import-organizer.ts │ └── index.ts ├── ioc-symbols.ts ├── ioc.ts ├── typescript-hero.ts └── utilities │ ├── logger.ts │ └── utility-functions.ts ├── test ├── imports │ ├── __snapshots__ │ │ ├── import-manager.test.ts.snap │ │ └── import-organizer.test.ts.snap │ ├── import-grouping │ │ ├── __snapshots__ │ │ │ ├── import-group-setting-parser.test.ts.snap │ │ │ ├── keyword-import-group.test.ts.snap │ │ │ ├── regex-import-group.test.ts.snap │ │ │ └── remain-import-group.test.ts.snap │ │ ├── import-group-setting-parser.test.ts │ │ ├── keyword-import-group.test.ts │ │ ├── regex-import-group.test.ts │ │ └── remain-import-group.test.ts │ ├── import-manager.test.ts │ └── import-organizer.test.ts ├── index.ts ├── setup.ts └── utilities │ ├── __snapshots__ │ └── utility-functions.test.ts.snap │ └── utility-functions.test.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 125 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test 4 | *.vsix 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | coverage/ -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:10 2 | 3 | stages: 4 | - test 5 | - release 6 | 7 | before_script: 8 | - npm ci 9 | - npm run postinstall 10 | - npm run build 11 | 12 | lint: 13 | stage: test 14 | script: 15 | - npm run lint 16 | except: 17 | - tags 18 | 19 | test: 20 | stage: test 21 | script: 22 | - npm test 23 | except: 24 | - tags 25 | 26 | release: 27 | stage: release 28 | script: 29 | - npx semantic-release 30 | only: 31 | - master 32 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["${workspaceRoot}/src", "--extensionDevelopmentPath=${workspaceRoot}"], 10 | "env": { 11 | "EXT_DEBUG": "true" 12 | }, 13 | "stopOnEntry": false, 14 | "sourceMaps": true, 15 | "outFiles": ["${workspaceRoot}/out/src/**/*.js"] 16 | }, 17 | { 18 | "name": "Launch Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test/"], 23 | "env": { 24 | "EXT_DEBUG": "true", 25 | "LOCAL_TEST": "true", 26 | "COVERAGE": "", 27 | "CHAI_JEST_SNAPSHOT_UPDATE_ALL": "" 28 | }, 29 | "stopOnEntry": false, 30 | "sourceMaps": true, 31 | "smartStep": true, 32 | "outFiles": ["${workspaceRoot}/out/**/*.js"] 33 | } 34 | ], 35 | "compounds": [] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } 4 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .github/** 2 | .vscode/** 3 | .vscode-test/** 4 | 5 | .gitignore 6 | package-lock.json 7 | prepare-release.js 8 | 9 | config/** 10 | test/** 11 | src/** 12 | !src/assets/** 13 | tsconfig.json 14 | tslint.json 15 | coverage/** 16 | 17 | __snapshots__/** 18 | 19 | *.vsix 20 | out/test/** 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Christoph Bühler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Hero 2 | 3 | TypeScript Hero is a vscode extension that makes your life easier. 4 | When you are coding a lot of `TypeScript` you may want vscode to organize your imports. 5 | 6 | If you'd like to buy me a beer :-) 7 | 8 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/rbbit) 9 | 10 | ## Features at a glance 11 | 12 | Here is a brief list, of what TypeScript Hero is capable of (more in the wiki): 13 | 14 | - Sort and organize your imports (sort and remove unused) 15 | - Code outline view of your open TS / TSX document 16 | - Add import to the document or add an import that is under the cursor to the document 17 | 18 | ## Known Issues 19 | 20 | Please visit [the issue list](https://gitlab.com/smartive/open-source/christoph/typescript-hero/issues) :-) 21 | -------------------------------------------------------------------------------- /config/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "../out", 6 | "moduleResolution": "node", 7 | "lib": [ 8 | "es6", 9 | "es2017" 10 | ], 11 | "rootDir": "../", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "strict": true, 17 | "importHelpers": true, 18 | "removeComments": true 19 | }, 20 | "include": [ 21 | "../src/**/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /config/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /config/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": true 5 | }, 6 | "include": ["../src/**/*", "../test/**/*"], 7 | "exclude": [] 8 | } 9 | -------------------------------------------------------------------------------- /etc/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buehler/typescript-hero/2cc666ec1d7d443aba1466a94e6e4c50a1424b83/etc/icon.png -------------------------------------------------------------------------------- /etc/test_install.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require('fs'); 2 | const { join } = require('path'); 3 | 4 | if (existsSync(join(__dirname, '..', 'node_modules', 'vscode', 'bin', 'install'))) { 5 | process.exit(0); 6 | return; 7 | } 8 | process.exit(1); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-hero", 3 | "displayName": "TypeScript Hero", 4 | "description": "Additional toolings for typescript", 5 | "icon": "etc/icon.png", 6 | "galleryBanner": { 7 | "color": "#1e324c", 8 | "theme": "dark" 9 | }, 10 | "version": "0.0.0-development", 11 | "publisher": "rbbit", 12 | "engines": { 13 | "vscode": "^1.28.0" 14 | }, 15 | "categories": [ 16 | "Formatters", 17 | "Programming Languages" 18 | ], 19 | "main": "./out/src/extension", 20 | "author": "Christoph Bühler ", 21 | "license": "MIT", 22 | "contributors": [], 23 | "badges": [], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://gitlab.com/smartive/open-source/christoph/typescript-hero.git" 27 | }, 28 | "bugs": { 29 | "url": "https://gitlab.com/smartive/open-source/christoph/typescript-hero/issues" 30 | }, 31 | "homepage": "https://gitlab.com/smartive/open-source/christoph/typescript-hero/", 32 | "scripts": { 33 | "postinstall": "node ./node_modules/vscode/bin/install", 34 | "clean": "del-cli ./out ./coverage", 35 | "build": "npm run clean && tsc -p ./config/tsconfig.build.json", 36 | "develop": "npm run clean && tsc -p .", 37 | "lint": "tslint -c ./tslint.json -p ./config/tsconfig.build.json", 38 | "test": "echo 'npm run lint && npm run clean && tsc -p ./config/tsconfig.test.json && node ./node_modules/vscode/bin/test'" 39 | }, 40 | "release": { 41 | "verifyConditions": [ 42 | "semantic-release-vsce", 43 | { 44 | "path": "@semantic-release/gitlab", 45 | "gitlabUrl": "https://gitlab.com" 46 | } 47 | ], 48 | "prepare": [ 49 | { 50 | "path": "semantic-release-vsce", 51 | "packageVsix": "rbbit.typescript-hero.vsix" 52 | } 53 | ], 54 | "publish": [ 55 | { 56 | "path": "semantic-release-vsce", 57 | "packageVsix": "rbbit.typescript-hero.vsix" 58 | }, 59 | { 60 | "path": "@semantic-release/gitlab", 61 | "gitlabUrl": "https://gitlab.com" 62 | } 63 | ], 64 | "success": false, 65 | "fail": false 66 | }, 67 | "devDependencies": { 68 | "@semantic-release/gitlab": "^3.0.4", 69 | "@smartive/tslint-config": "^4.0.0", 70 | "@types/chai": "^4.1.6", 71 | "@types/fs-extra": "^5.0.4", 72 | "@types/glob": "^7.1.1", 73 | "@types/istanbul": "^0.4.30", 74 | "@types/mocha": "^5.2.5", 75 | "@types/node": "^10.11.5", 76 | "@types/reflect-metadata": "0.1.0", 77 | "@types/sinon": "^5.0.5", 78 | "@types/sinon-chai": "^3.2.0", 79 | "chai": "^4.2.0", 80 | "chai-jest-snapshot": "^2.0.0", 81 | "decache": "^4.4.0", 82 | "del-cli": "^1.1.0", 83 | "istanbul": "^0.4.5", 84 | "remap-istanbul": "^0.12.0", 85 | "semantic-release": "^15.9.17", 86 | "semantic-release-vsce": "^2.1.2", 87 | "sinon": "^6.3.5", 88 | "sinon-chai": "^3.2.0", 89 | "tslint": "^5.11.0", 90 | "tsutils": "^3.0.0", 91 | "vscode": "^1.1.21" 92 | }, 93 | "dependencies": { 94 | "fs-extra": "^7.0.0", 95 | "inversify": "^4.13.0", 96 | "reflect-metadata": "^0.1.12", 97 | "tslib": "^1.9.3", 98 | "typescript": "~3.1.1", 99 | "typescript-parser": "^2.6.1", 100 | "winston": "^3.1.0" 101 | }, 102 | "activationEvents": [ 103 | "onLanguage:typescript", 104 | "onLanguage:typescriptreact", 105 | "onLanguage:javascript", 106 | "onLanguage:javascriptreact" 107 | ], 108 | "contributes": { 109 | "commands": [ 110 | { 111 | "command": "typescriptHero.imports.organize", 112 | "title": "TS Hero: Organize imports (sort and remove unused)" 113 | } 114 | ], 115 | "keybindings": [ 116 | { 117 | "command": "typescriptHero.imports.organize", 118 | "key": "ctrl+alt+o", 119 | "when": "editorTextFocus" 120 | } 121 | ], 122 | "configuration": { 123 | "title": "TypeScript Hero", 124 | "properties": { 125 | "typescriptHero.verbosity": { 126 | "enum": [ 127 | "error", 128 | "warn", 129 | "info", 130 | "debug" 131 | ], 132 | "default": "warn", 133 | "description": "Defines the log output level in the output window. In the log file, it's always info or debug.", 134 | "scope": "window" 135 | }, 136 | "typescriptHero.imports.insertSpaceBeforeAndAfterImportBraces": { 137 | "type": "boolean", 138 | "default": true, 139 | "description": "Defines if there should be a space inside the curly braces of an import statement.", 140 | "scope": "resource" 141 | }, 142 | "typescriptHero.imports.removeTrailingIndex": { 143 | "type": "boolean", 144 | "default": true, 145 | "description": "Defines if a trailing '/index' should be removed from imports.", 146 | "scope": "resource" 147 | }, 148 | "typescriptHero.imports.insertSemicolons": { 149 | "type": "boolean", 150 | "default": true, 151 | "description": "Defines if there should be a semicolon at the end of a statement.", 152 | "scope": "resource" 153 | }, 154 | "typescriptHero.imports.stringQuoteStyle": { 155 | "enum": [ 156 | "'", 157 | "\"" 158 | ], 159 | "default": "'", 160 | "description": "Defines if single or double quotes should be used.", 161 | "scope": "resource" 162 | }, 163 | "typescriptHero.imports.multiLineWrapThreshold": { 164 | "type": "number", 165 | "minimum": 1, 166 | "multipleOf": 1, 167 | "default": 125, 168 | "description": "Defines the threshold when an import should be wrapped into a multiline import.", 169 | "scope": "resource" 170 | }, 171 | "typescriptHero.imports.multiLineTrailingComma": { 172 | "type": "boolean", 173 | "default": true, 174 | "description": "Defined if multi line imports contain the last trailing comma.", 175 | "scope": "resource" 176 | }, 177 | "typescriptHero.imports.organizeOnSave": { 178 | "type": "boolean", 179 | "default": false, 180 | "description": "Defines if the imports should be organized on save.", 181 | "scope": "resource" 182 | }, 183 | "typescriptHero.imports.organizeSortsByFirstSpecifier": { 184 | "type": "boolean", 185 | "default": false, 186 | "description": "Defines if the imports are organized by first specifier/alias instead of module path.", 187 | "scope": "resource" 188 | }, 189 | "typescriptHero.imports.disableImportsSorting": { 190 | "type": "boolean", 191 | "default": false, 192 | "description": "Defines if sorting is disable during organize imports.", 193 | "scope": "resource" 194 | }, 195 | "typescriptHero.imports.disableImportRemovalOnOrganize": { 196 | "type": "boolean", 197 | "default": false, 198 | "description": "Defines if any imports should be removed at all on an organize imports command.", 199 | "scope": "resource" 200 | }, 201 | "typescriptHero.imports.ignoredFromRemoval": { 202 | "type": "array", 203 | "items": { 204 | "type": "string" 205 | }, 206 | "uniqueItems": true, 207 | "default": [ 208 | "react" 209 | ], 210 | "description": "Defines imports (libraries, so the 'from' part), which are not removed during 'organize imports'.", 211 | "scope": "resource" 212 | }, 213 | "typescriptHero.imports.grouping": { 214 | "type": "array", 215 | "items": { 216 | "anyOf": [ 217 | { 218 | "enum": [ 219 | "Modules", 220 | "Plains", 221 | "Workspace", 222 | "Remaining" 223 | ] 224 | }, 225 | { 226 | "type": "string", 227 | "pattern": "\\/[A-Za-z-_0-9]+\\/" 228 | }, 229 | { 230 | "type": "object", 231 | "properties": { 232 | "identifier": { 233 | "enum": [ 234 | "Modules", 235 | "Plains", 236 | "Workspace", 237 | "Remaining" 238 | ] 239 | }, 240 | "order": { 241 | "enum": [ 242 | "asc", 243 | "desc" 244 | ] 245 | } 246 | }, 247 | "additionalProperties": false, 248 | "required": [ 249 | "identifier", 250 | "order" 251 | ] 252 | }, 253 | { 254 | "type": "object", 255 | "properties": { 256 | "identifier": { 257 | "type": "string", 258 | "pattern": "\\/[A-Za-z-_0-9]+\\/" 259 | }, 260 | "order": { 261 | "enum": [ 262 | "asc", 263 | "desc" 264 | ] 265 | } 266 | }, 267 | "additionalProperties": false, 268 | "required": [ 269 | "identifier", 270 | "order" 271 | ] 272 | } 273 | ] 274 | }, 275 | "default": [ 276 | "Plains", 277 | "Modules", 278 | "Workspace" 279 | ], 280 | "description": "Defines the groups of the imports ordering. Multiple groups possible, see readme for instructions.", 281 | "scope": "resource" 282 | } 283 | } 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/activatable.ts: -------------------------------------------------------------------------------- 1 | export interface Activatable { 2 | setup(): void; 3 | start(): void; 4 | stop(): void; 5 | dispose(): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/configuration/imports-config.ts: -------------------------------------------------------------------------------- 1 | import { Uri, workspace } from 'vscode'; 2 | 3 | import { ImportGroup, ImportGroupSetting, ImportGroupSettingParser, RemainImportGroup } from '../imports/import-grouping'; 4 | 5 | const sectionKey = 'typescriptHero.imports'; 6 | 7 | export class ImportsConfig { 8 | public insertSpaceBeforeAndAfterImportBraces(resource: Uri): boolean { 9 | return workspace 10 | .getConfiguration(sectionKey, resource) 11 | .get('insertSpaceBeforeAndAfterImportBraces', true); 12 | } 13 | 14 | public insertSemicolons(resource: Uri): boolean { 15 | return workspace 16 | .getConfiguration(sectionKey, resource) 17 | .get('insertSemicolons', true); 18 | } 19 | 20 | public removeTrailingIndex(resource: Uri): boolean { 21 | return workspace 22 | .getConfiguration(sectionKey, resource) 23 | .get('removeTrailingIndex', true); 24 | } 25 | 26 | public stringQuoteStyle(resource: Uri): '"' | '\'' { 27 | return workspace 28 | .getConfiguration(sectionKey, resource) 29 | .get('stringQuoteStyle', `'`); 30 | } 31 | 32 | public multiLineWrapThreshold(resource: Uri): number { 33 | return workspace 34 | .getConfiguration(sectionKey, resource) 35 | .get('multiLineWrapThreshold', 125); 36 | } 37 | 38 | public multiLineTrailingComma(resource: Uri): boolean { 39 | return workspace 40 | .getConfiguration(sectionKey, resource) 41 | .get('multiLineTrailingComma', true); 42 | } 43 | 44 | public disableImportRemovalOnOrganize(resource: Uri): boolean { 45 | return workspace 46 | .getConfiguration(sectionKey, resource) 47 | .get('disableImportRemovalOnOrganize', false); 48 | } 49 | 50 | public disableImportsSorting(resource: Uri): boolean { 51 | return workspace 52 | .getConfiguration(sectionKey, resource) 53 | .get('disableImportsSorting', false); 54 | } 55 | 56 | public organizeOnSave(resource: Uri): boolean { 57 | return workspace 58 | .getConfiguration(sectionKey, resource) 59 | .get('organizeOnSave', false); 60 | } 61 | 62 | public organizeSortsByFirstSpecifier(resource: Uri): boolean { 63 | return workspace 64 | .getConfiguration(sectionKey, resource) 65 | .get('organizeSortsByFirstSpecifier', false); 66 | } 67 | 68 | public ignoredFromRemoval(resource: Uri): string[] { 69 | return workspace 70 | .getConfiguration(sectionKey, resource) 71 | .get('ignoredFromRemoval', ['react']); 72 | } 73 | 74 | public grouping(resource: Uri): ImportGroup[] { 75 | const groups = workspace 76 | .getConfiguration(sectionKey, resource) 77 | .get('grouping'); 78 | let importGroups: ImportGroup[] = []; 79 | 80 | try { 81 | if (groups) { 82 | importGroups = groups.map(g => 83 | ImportGroupSettingParser.parseSetting(g), 84 | ); 85 | } else { 86 | importGroups = ImportGroupSettingParser.default; 87 | } 88 | } catch (e) { 89 | importGroups = ImportGroupSettingParser.default; 90 | } 91 | if (!importGroups.some(i => i instanceof RemainImportGroup)) { 92 | importGroups.push(new RemainImportGroup()); 93 | } 94 | 95 | return importGroups; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/configuration/index.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { MultiLineImportRule, TypescriptGenerationOptions } from 'typescript-parser'; 3 | import { Event, EventEmitter, ExtensionContext, Uri, window, workspace } from 'vscode'; 4 | 5 | import { iocSymbols } from '../ioc-symbols'; 6 | import { ImportsConfig } from './imports-config'; 7 | 8 | const sectionKey = 'typescriptHero'; 9 | 10 | @injectable() 11 | export class Configuration { 12 | public readonly imports: ImportsConfig = new ImportsConfig(); 13 | 14 | private readonly _configurationChanged: EventEmitter< 15 | void 16 | > = new EventEmitter(); 17 | 18 | public get configurationChanged(): Event { 19 | return this._configurationChanged.event; 20 | } 21 | 22 | constructor(@inject(iocSymbols.extensionContext) context: ExtensionContext) { 23 | context.subscriptions.push( 24 | workspace.onDidChangeConfiguration(() => 25 | this._configurationChanged.fire(), 26 | ), 27 | ); 28 | context.subscriptions.push(this._configurationChanged); 29 | } 30 | 31 | public parseableLanguages(): string[] { 32 | return ['typescript', 'typescriptreact', 'javascript', 'javascriptreact']; 33 | } 34 | 35 | public verbosity(): 'error' | 'warn' | 'info' | 'debug' { 36 | const verbosity = workspace 37 | .getConfiguration(sectionKey) 38 | .get<'error' | 'warn' | 'info' | 'debug'>('verbosity', 'warn'); 39 | if (['error', 'warn', 'info', 'debug'].indexOf(verbosity) < 0) { 40 | return 'warn'; 41 | } 42 | return verbosity; 43 | } 44 | 45 | public typescriptGeneratorOptions( 46 | resource: Uri, 47 | ): TypescriptGenerationOptions { 48 | return { 49 | eol: this.imports.insertSemicolons(resource) ? ';' : '', 50 | insertSpaces: true, 51 | multiLineTrailingComma: this.imports.multiLineTrailingComma(resource), 52 | multiLineWrapThreshold: this.imports.multiLineWrapThreshold(resource), 53 | spaceBraces: this.imports.insertSpaceBeforeAndAfterImportBraces(resource), 54 | stringQuoteStyle: this.imports.stringQuoteStyle(resource), 55 | tabSize: 56 | window.activeTextEditor && window.activeTextEditor.options.tabSize 57 | ? (window.activeTextEditor.options.tabSize as any) * 1 58 | : workspace.getConfiguration('editor', resource).get('tabSize', 4), 59 | wrapMethod: MultiLineImportRule.oneImportPerLineOnlyAfterThreshold, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { ExtensionContext } from 'vscode'; 4 | 5 | import { Activatable } from './activatable'; 6 | import { ioc } from './ioc'; 7 | import { iocSymbols } from './ioc-symbols'; 8 | import { TypescriptHero } from './typescript-hero'; 9 | 10 | let extension: Activatable; 11 | 12 | /** 13 | * Activates TypeScript Hero 14 | * 15 | * @export 16 | * @param {ExtensionContext} context 17 | */ 18 | export async function activate(context: ExtensionContext): Promise { 19 | if (ioc.isBound(iocSymbols.extensionContext)) { 20 | ioc.unbind(iocSymbols.extensionContext); 21 | } 22 | ioc 23 | .bind(iocSymbols.extensionContext) 24 | .toConstantValue(context); 25 | 26 | extension = ioc.get(TypescriptHero); 27 | 28 | extension.setup(); 29 | extension.start(); 30 | } 31 | 32 | /** 33 | * Deactivates TypeScript Hero 34 | * 35 | * @export 36 | */ 37 | export function deactivate(): void { 38 | extension.stop(); 39 | extension.dispose(); 40 | } 41 | -------------------------------------------------------------------------------- /src/imports/import-grouping/import-group-identifier-invalid-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Thrown when a import group identifier neither does match a keyword nor a regex pattern. 3 | * 4 | * @export 5 | * @class ImportGroupIdentifierInvalidError 6 | * @extends {Error} 7 | */ 8 | export class ImportGroupIdentifierInvalidError extends Error { 9 | constructor(identifier: string) { 10 | super(); 11 | this.message = `The identifier "${identifier}" does not match a keyword or a regex pattern (/ .. /).`; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/imports/import-grouping/import-group-keyword.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for the different special keywords of the KeywordImportGroup. 3 | * 4 | * @export 5 | * @enum {number} 6 | */ 7 | export enum ImportGroupKeyword { 8 | Modules = 'Modules', 9 | Plains = 'Plains', 10 | Workspace = 'Workspace', 11 | Remaining = 'Remaining', 12 | } 13 | -------------------------------------------------------------------------------- /src/imports/import-grouping/import-group-order.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Import grouping order (asc / desc). 3 | * 4 | * @export 5 | * @enum {string} 6 | */ 7 | export enum ImportGroupOrder { 8 | Asc = 'asc', 9 | Desc = 'desc', 10 | } 11 | -------------------------------------------------------------------------------- /src/imports/import-grouping/import-group-setting-parser.ts: -------------------------------------------------------------------------------- 1 | import { ImportGroup } from './import-group'; 2 | import { ImportGroupIdentifierInvalidError } from './import-group-identifier-invalid-error'; 3 | import { ImportGroupKeyword } from './import-group-keyword'; 4 | import { ImportGroupOrder } from './import-group-order'; 5 | import { KeywordImportGroup } from './keyword-import-group'; 6 | import { RegexImportGroup } from './regex-import-group'; 7 | import { RemainImportGroup } from './remain-import-group'; 8 | 9 | /** 10 | * Inserted setting that is contained in the settings.json of .vscode. 11 | */ 12 | export type ImportGroupSetting = 13 | | ImportGroupKeyword 14 | | string 15 | | { identifier: ImportGroupKeyword | string; order: ImportGroupOrder }; 16 | 17 | const REGEX_REGEX_GROUP = /^\/.+\/$/; 18 | 19 | /** 20 | * Parser that takes the vscode - setting and creates import groups out of it. 21 | * Contains a default if the parsing fails. 22 | * 23 | * @export 24 | * @class ImportGroupSettingParser 25 | */ 26 | export class ImportGroupSettingParser { 27 | /** 28 | * Default value for the import groups. 29 | * Contains the following: 30 | * - Plain imports 31 | * - Module imports 32 | * - Workspace imports 33 | * 34 | * @readonly 35 | * @static 36 | * @type {ImportGroup[]} 37 | * @memberof ImportGroupSettingParser 38 | */ 39 | public static get default(): ImportGroup[] { 40 | return [ 41 | new KeywordImportGroup(ImportGroupKeyword.Plains), 42 | new KeywordImportGroup(ImportGroupKeyword.Modules), 43 | new KeywordImportGroup(ImportGroupKeyword.Workspace), 44 | new RemainImportGroup(), 45 | ]; 46 | } 47 | 48 | /** 49 | * Function that takes a string or object ({@link ImportGroupSetting}) and parses an import group out of it. 50 | * 51 | * @static 52 | * @param {ImportGroupSetting} setting 53 | * @returns {ImportGroup} 54 | * @throws {ImportGroupIdentifierInvalidError} When the identifier is invalid (neither keyword nor valid regex) 55 | * 56 | * @memberof ImportGroupSettingParser 57 | */ 58 | public static parseSetting(setting: ImportGroupSetting): ImportGroup { 59 | let identifier: ImportGroupKeyword | string; 60 | let order: ImportGroupOrder = ImportGroupOrder.Asc; 61 | 62 | if (typeof setting === 'string') { 63 | identifier = setting; 64 | } else { 65 | identifier = setting.identifier; 66 | order = setting.order; 67 | } 68 | 69 | if (REGEX_REGEX_GROUP.test(identifier)) { 70 | return new RegexImportGroup(identifier, order); 71 | } 72 | 73 | if (identifier === ImportGroupKeyword.Remaining) { 74 | return new RemainImportGroup(order); 75 | } 76 | 77 | if (ImportGroupKeyword[identifier as any] !== undefined) { 78 | return new KeywordImportGroup( 79 | (ImportGroupKeyword as any)[identifier as any], 80 | order, 81 | ); 82 | } 83 | 84 | throw new ImportGroupIdentifierInvalidError(identifier); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/imports/import-grouping/import-group.ts: -------------------------------------------------------------------------------- 1 | import { Import } from 'typescript-parser'; 2 | 3 | import { ImportGroupOrder } from './import-group-order'; 4 | 5 | /** 6 | * Interface for an import group. A group contains a list of imports that are grouped and sorted 7 | * together on organizing all imports and inserting new imports. 8 | * 9 | * @export 10 | * @interface ImportGroup 11 | * @extends {Generatable} 12 | */ 13 | export interface ImportGroup { 14 | /** 15 | * The readonly list of imports for this group. 16 | * 17 | * @type {Import[]} 18 | * @memberof ImportGroup 19 | */ 20 | readonly imports: Import[]; 21 | 22 | /** 23 | * A sorted list of the imports of this group. 24 | * 25 | * @type {Import[]} 26 | * @memberof ImportGroup 27 | */ 28 | readonly sortedImports: Import[]; 29 | 30 | /** 31 | * The order of the imports (asc / desc). 32 | * 33 | * @type {ImportGroupOrder} 34 | * @memberof ImportGroup 35 | */ 36 | order: ImportGroupOrder; 37 | 38 | /** 39 | * Adds the given import to itself if it is the correct group for the import. Does return true if the import is 40 | * handled, otherwise it must return false. 41 | * 42 | * @param {Import} tsImport 43 | * @returns {boolean} 44 | * 45 | * @memberof ImportGroup 46 | */ 47 | processImport(tsImport: Import): boolean; 48 | 49 | /** 50 | * Resets the import group (clears the imports). 51 | * 52 | * 53 | * @memberof ImportGroup 54 | */ 55 | reset(): void; 56 | } 57 | -------------------------------------------------------------------------------- /src/imports/import-grouping/index.ts: -------------------------------------------------------------------------------- 1 | export * from './import-group'; 2 | export * from './import-group-identifier-invalid-error'; 3 | export * from './import-group-keyword'; 4 | export * from './import-group-order'; 5 | export * from './import-group-setting-parser'; 6 | export * from './keyword-import-group'; 7 | export * from './regex-import-group'; 8 | export * from './remain-import-group'; 9 | -------------------------------------------------------------------------------- /src/imports/import-grouping/keyword-import-group.ts: -------------------------------------------------------------------------------- 1 | import { Import, StringImport } from 'typescript-parser'; 2 | 3 | import { importSort } from '../../utilities/utility-functions'; 4 | import { ImportGroup } from './import-group'; 5 | import { ImportGroupKeyword } from './import-group-keyword'; 6 | import { ImportGroupOrder } from './import-group-order'; 7 | 8 | /** 9 | * Importgroup for keywords. Uses "Modules", "Plains", "Workspace" as a keyword and processes the corresponding imports. 10 | * 11 | * @export 12 | * @class KeywordImportGroup 13 | * @implements {ImportGroup} 14 | */ 15 | export class KeywordImportGroup implements ImportGroup { 16 | public readonly imports: Import[] = []; 17 | 18 | public get sortedImports(): Import[] { 19 | return this.imports.sort((i1, i2) => importSort(i1, i2, this.order)); 20 | } 21 | 22 | constructor( 23 | public readonly keyword: ImportGroupKeyword, 24 | public readonly order: ImportGroupOrder = ImportGroupOrder.Asc, 25 | ) {} 26 | 27 | public reset(): void { 28 | this.imports.length = 0; 29 | } 30 | 31 | public processImport(tsImport: Import): boolean { 32 | switch (this.keyword) { 33 | case ImportGroupKeyword.Modules: 34 | return this.processModulesImport(tsImport); 35 | case ImportGroupKeyword.Plains: 36 | return this.processPlainsImport(tsImport); 37 | case ImportGroupKeyword.Workspace: 38 | return this.processWorkspaceImport(tsImport); 39 | default: 40 | return false; 41 | } 42 | } 43 | 44 | /** 45 | * Process a library import. 46 | * @example import ... from 'vscode'; 47 | * 48 | * @private 49 | * @param {Import} tsImport 50 | * @returns {boolean} 51 | * 52 | * @memberof KeywordImportGroup 53 | */ 54 | private processModulesImport(tsImport: Import): boolean { 55 | if ( 56 | tsImport instanceof StringImport || 57 | tsImport.libraryName.startsWith('.') || 58 | tsImport.libraryName.startsWith('/') 59 | ) { 60 | return false; 61 | } 62 | this.imports.push(tsImport); 63 | return true; 64 | } 65 | 66 | /** 67 | * Process a string only import. 68 | * @example import 'reflect-metadata'; 69 | * 70 | * @private 71 | * @param {Import} tsImport 72 | * @returns {boolean} 73 | * 74 | * @memberof KeywordImportGroup 75 | */ 76 | private processPlainsImport(tsImport: Import): boolean { 77 | if (!(tsImport instanceof StringImport)) { 78 | return false; 79 | } 80 | this.imports.push(tsImport); 81 | return true; 82 | } 83 | 84 | /** 85 | * Process a workspace import (not string nor lib import). 86 | * @example import ... from './server'; 87 | * 88 | * @private 89 | * @param {Import} tsImport 90 | * @returns {boolean} 91 | * 92 | * @memberof KeywordImportGroup 93 | */ 94 | private processWorkspaceImport(tsImport: Import): boolean { 95 | if ( 96 | tsImport instanceof StringImport || 97 | (!tsImport.libraryName.startsWith('.') && 98 | !tsImport.libraryName.startsWith('/')) 99 | ) { 100 | return false; 101 | } 102 | this.imports.push(tsImport); 103 | return true; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/imports/import-grouping/regex-import-group.ts: -------------------------------------------------------------------------------- 1 | import { Import, StringImport } from 'typescript-parser'; 2 | 3 | import { importSort } from '../../utilities/utility-functions'; 4 | import { ImportGroup } from './import-group'; 5 | import { ImportGroupOrder } from './import-group-order'; 6 | 7 | /** 8 | * Importgroup that processes all imports that match a certain regex (the lib name). 9 | * 10 | * @export 11 | * @class RegexImportGroup 12 | * @implements {ImportGroup} 13 | */ 14 | export class RegexImportGroup implements ImportGroup { 15 | public readonly imports: Import[] = []; 16 | 17 | public get sortedImports(): Import[] { 18 | const sorted = this.imports.sort((i1, i2) => 19 | importSort(i1, i2, this.order), 20 | ); 21 | return [ 22 | ...sorted.filter(i => i instanceof StringImport), 23 | ...sorted.filter(i => !(i instanceof StringImport)), 24 | ]; 25 | } 26 | 27 | /** 28 | * Creates an instance of RegexImportGroup. 29 | * 30 | * @param {string} regex The regex that is matched against the imports library name. 31 | * @param {ImportGroupOrder} [order='asc'] 32 | * 33 | * @memberof RegexImportGroup 34 | */ 35 | constructor( 36 | public readonly regex: string, 37 | public readonly order: ImportGroupOrder = ImportGroupOrder.Asc, 38 | ) {} 39 | 40 | public reset(): void { 41 | this.imports.length = 0; 42 | } 43 | 44 | public processImport(tsImport: Import): boolean { 45 | let regexString = this.regex; 46 | regexString = regexString.startsWith('/') 47 | ? regexString.substring(1) 48 | : regexString; 49 | regexString = regexString.endsWith('/') 50 | ? regexString.substring(0, regexString.length - 1) 51 | : regexString; 52 | const regex = new RegExp(regexString, 'g'); 53 | 54 | if (regex.test(tsImport.libraryName)) { 55 | this.imports.push(tsImport); 56 | return true; 57 | } 58 | return false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/imports/import-grouping/remain-import-group.ts: -------------------------------------------------------------------------------- 1 | import { Import, StringImport } from 'typescript-parser'; 2 | 3 | import { importSort } from '../../utilities/utility-functions'; 4 | import { ImportGroup } from './import-group'; 5 | import { ImportGroupOrder } from './import-group-order'; 6 | 7 | /** 8 | * Importgroup that processes all imports. Should be used if other groups don't process the import. 9 | * 10 | * @export 11 | * @class RemainImportGroup 12 | * @implements {ImportGroup} 13 | */ 14 | export class RemainImportGroup implements ImportGroup { 15 | public readonly imports: Import[] = []; 16 | 17 | public get sortedImports(): Import[] { 18 | const sorted = this.imports.sort((i1, i2) => 19 | importSort(i1, i2, this.order), 20 | ); 21 | return [ 22 | ...sorted.filter(i => i instanceof StringImport), 23 | ...sorted.filter(i => !(i instanceof StringImport)), 24 | ]; 25 | } 26 | 27 | constructor(public readonly order: ImportGroupOrder = ImportGroupOrder.Asc) {} 28 | 29 | public reset(): void { 30 | this.imports.length = 0; 31 | } 32 | 33 | public processImport(tsImport: Import): boolean { 34 | this.imports.push(tsImport); 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/imports/import-manager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeclarationInfo, 3 | DefaultDeclaration, 4 | ExternalModuleImport, 5 | File, 6 | Import, 7 | ModuleDeclaration, 8 | NamedImport, 9 | NamespaceImport, 10 | StringImport, 11 | SymbolSpecifier, 12 | TypescriptCodeGenerator, 13 | TypescriptParser, 14 | } from 'typescript-parser'; 15 | import { Position, Range, TextDocument, TextEdit, window, workspace, WorkspaceEdit } from 'vscode'; 16 | 17 | import { Configuration } from '../configuration'; 18 | import { TypescriptCodeGeneratorFactory } from '../ioc-symbols'; 19 | import { Logger } from '../utilities/logger'; 20 | import { 21 | getAbsolutLibraryName, 22 | getImportInsertPosition, 23 | getRelativeLibraryName, 24 | getScriptKind, 25 | importGroupSortForPrecedence, 26 | importSort, 27 | importSortByFirstSpecifier, 28 | specifierSort, 29 | } from '../utilities/utility-functions'; 30 | import { ImportGroup } from './import-grouping'; 31 | 32 | function sameSpecifiers( 33 | specs1: SymbolSpecifier[], 34 | specs2: SymbolSpecifier[], 35 | ): boolean { 36 | for (const spec of specs1) { 37 | const spec2 = specs2[specs1.indexOf(spec)]; 38 | if ( 39 | !spec2 || 40 | spec.specifier !== spec2.specifier || 41 | spec.alias !== spec2.alias 42 | ) { 43 | return false; 44 | } 45 | } 46 | return true; 47 | } 48 | 49 | /** 50 | * Function that calculates the range object for an import. 51 | * 52 | * @export 53 | * @param {TextDocument} document 54 | * @param {number} [start] 55 | * @param {number} [end] 56 | * @returns {Range} 57 | */ 58 | export function importRange( 59 | document: TextDocument, 60 | start?: number, 61 | end?: number, 62 | ): Range { 63 | return start !== undefined && end !== undefined 64 | ? new Range( 65 | document.lineAt( 66 | document.positionAt(start).line, 67 | ).rangeIncludingLineBreak.start, 68 | document.lineAt( 69 | document.positionAt(end).line, 70 | ).rangeIncludingLineBreak.end, 71 | ) 72 | : new Range(new Position(0, 0), new Position(0, 0)); 73 | } 74 | 75 | /** 76 | * Management class for the imports of a document. Can add and remove imports to the document 77 | * and commit the virtual document to the TextEditor. 78 | * 79 | * @export 80 | * @class ImportManager 81 | */ 82 | export class ImportManager { 83 | private importGroups: ImportGroup[] = []; 84 | private imports: Import[] = []; 85 | private organize: boolean = false; 86 | 87 | private get rootPath(): string | undefined { 88 | const rootFolder = workspace.getWorkspaceFolder(this.document.uri); 89 | return rootFolder ? rootFolder.uri.fsPath : undefined; 90 | } 91 | 92 | private get generator(): TypescriptCodeGenerator { 93 | return this.generatorFactory(this.document.uri); 94 | } 95 | 96 | /** 97 | * Document resource for this controller. Contains the parsed document. 98 | * 99 | * @readonly 100 | * @type {File} 101 | * @memberof ImportManager 102 | */ 103 | public get parsedDocument(): File { 104 | return this._parsedDocument; 105 | } 106 | 107 | public constructor( 108 | public readonly document: TextDocument, 109 | private _parsedDocument: File, 110 | private readonly parser: TypescriptParser, 111 | private readonly config: Configuration, 112 | private readonly logger: Logger, 113 | private readonly generatorFactory: TypescriptCodeGeneratorFactory, 114 | ) { 115 | this.logger.debug(`[ImportManager] Create import manager`, { 116 | file: document.fileName, 117 | }); 118 | this.reset(); 119 | } 120 | 121 | /** 122 | * Resets the imports and the import groups back to the initial state of the parsed document. 123 | * 124 | * @memberof ImportManager 125 | */ 126 | public reset(): void { 127 | this.imports = this._parsedDocument.imports.map(o => o.clone()); 128 | this.importGroups = this.config.imports.grouping(this.document.uri); 129 | this.addImportsToGroups(this.imports); 130 | } 131 | 132 | /** 133 | * Organizes the imports of the document. Orders all imports and removes unused imports. 134 | * Order: 135 | * 1. string-only imports (e.g. import 'reflect-metadata') 136 | * 2. rest, but in alphabetical order 137 | * 138 | * @returns {ImportManager} 139 | * 140 | * @memberof ImportManager 141 | */ 142 | public organizeImports(): this { 143 | this.logger.debug('[ImportManager] Organize the imports', { 144 | file: this.document.fileName, 145 | }); 146 | this.organize = true; 147 | let keep: Import[] = []; 148 | 149 | if (this.config.imports.disableImportRemovalOnOrganize(this.document.uri)) { 150 | keep = this.imports; 151 | } else { 152 | for (const actImport of this.imports) { 153 | if ( 154 | this.config.imports 155 | .ignoredFromRemoval(this.document.uri) 156 | .indexOf(actImport.libraryName) >= 0 157 | ) { 158 | keep.push(actImport); 159 | continue; 160 | } 161 | if ( 162 | actImport instanceof NamespaceImport || 163 | actImport instanceof ExternalModuleImport 164 | ) { 165 | if ( 166 | this._parsedDocument.nonLocalUsages.indexOf(actImport.alias) > -1 167 | ) { 168 | keep.push(actImport); 169 | } 170 | } else if (actImport instanceof NamedImport) { 171 | actImport.specifiers = actImport.specifiers 172 | .filter( 173 | o => 174 | this._parsedDocument.nonLocalUsages.indexOf( 175 | o.alias || o.specifier, 176 | ) > -1, 177 | ) 178 | .sort(specifierSort); 179 | const defaultSpec = actImport.defaultAlias; 180 | const libraryAlreadyImported = keep.find( 181 | d => d.libraryName === actImport.libraryName, 182 | ); 183 | if ( 184 | actImport.specifiers.length || 185 | (!!defaultSpec && 186 | [ 187 | ...this._parsedDocument.nonLocalUsages, 188 | ...this._parsedDocument.usages, 189 | ].indexOf(defaultSpec) >= 0) 190 | ) { 191 | if (libraryAlreadyImported) { 192 | if (actImport.defaultAlias) { 193 | (libraryAlreadyImported).defaultAlias = 194 | actImport.defaultAlias; 195 | } 196 | (libraryAlreadyImported).specifiers = [ 197 | ...(libraryAlreadyImported).specifiers, 198 | ...actImport.specifiers, 199 | ]; 200 | } else { 201 | keep.push(actImport); 202 | } 203 | } 204 | } else if (actImport instanceof StringImport) { 205 | keep.push(actImport); 206 | } 207 | } 208 | } 209 | 210 | if (!this.config.imports.disableImportsSorting(this.document.uri)) { 211 | const sorter = this.config.imports.organizeSortsByFirstSpecifier( 212 | this.document.uri, 213 | ) 214 | ? importSortByFirstSpecifier 215 | : importSort; 216 | 217 | keep = [ 218 | ...keep.filter(o => o instanceof StringImport).sort(sorter), 219 | ...keep.filter(o => !(o instanceof StringImport)).sort(sorter), 220 | ]; 221 | } 222 | 223 | if (this.config.imports.removeTrailingIndex(this.document.uri)) { 224 | for (const imp of keep.filter(lib => 225 | lib.libraryName.endsWith('/index'), 226 | )) { 227 | imp.libraryName = imp.libraryName.replace(/\/index$/, ''); 228 | } 229 | } 230 | 231 | for (const group of this.importGroups) { 232 | group.reset(); 233 | } 234 | this.imports = keep; 235 | this.addImportsToGroups(this.imports); 236 | 237 | return this; 238 | } 239 | 240 | /** 241 | * Adds an import for a declaration to the documents imports. 242 | * This index is merged and commited during the commit() function. 243 | * If it's a default import or there is a duplicate identifier, the controller will ask for the name on commit(). 244 | * 245 | * @param {DeclarationInfo} declarationInfo The import that should be added to the document 246 | * @returns {ImportManager} 247 | * 248 | * @memberof ImportManager 249 | */ 250 | public addDeclarationImport(declarationInfo: DeclarationInfo): this { 251 | this.logger.debug('[ImportManager] Add declaration as import', { 252 | file: this.document.fileName, 253 | specifier: declarationInfo.declaration.name, 254 | library: declarationInfo.from, 255 | }); 256 | // If there is something already imported, it must be a NamedImport 257 | const alreadyImported: NamedImport = this.imports.find( 258 | o => 259 | declarationInfo.from === 260 | getAbsolutLibraryName( 261 | o.libraryName, 262 | this.document.fileName, 263 | this.rootPath, 264 | ) && o instanceof NamedImport, 265 | ) as NamedImport; 266 | 267 | if (alreadyImported) { 268 | // If we found an import for this declaration, it's named import (with a possible default declaration) 269 | if (declarationInfo.declaration instanceof DefaultDeclaration) { 270 | delete alreadyImported.defaultAlias; 271 | alreadyImported.defaultAlias = declarationInfo.declaration.name; 272 | } else if ( 273 | !alreadyImported.specifiers.some( 274 | o => o.specifier === declarationInfo.declaration.name, 275 | ) 276 | ) { 277 | alreadyImported.specifiers.push( 278 | new SymbolSpecifier(declarationInfo.declaration.name), 279 | ); 280 | } 281 | } else { 282 | let imp: Import = new NamedImport( 283 | getRelativeLibraryName( 284 | declarationInfo.from, 285 | this.document.fileName, 286 | this.rootPath, 287 | ), 288 | ); 289 | 290 | if (declarationInfo.declaration instanceof ModuleDeclaration) { 291 | imp = new NamespaceImport( 292 | declarationInfo.from, 293 | declarationInfo.declaration.name, 294 | ); 295 | } else if (declarationInfo.declaration instanceof DefaultDeclaration) { 296 | (imp as NamedImport).defaultAlias = declarationInfo.declaration.name; 297 | } else { 298 | (imp as NamedImport).specifiers.push( 299 | new SymbolSpecifier(declarationInfo.declaration.name), 300 | ); 301 | } 302 | this.imports.push(imp); 303 | this.addImportsToGroups([imp]); 304 | } 305 | 306 | return this; 307 | } 308 | 309 | /** 310 | * Does commit the currently virtual document to the TextEditor. 311 | * Returns a promise that resolves to a boolean if all changes 312 | * could be applied. 313 | * 314 | * @returns {Promise} 315 | * 316 | * @memberof ImportManager 317 | */ 318 | public async commit(): Promise { 319 | const edits: TextEdit[] = this.calculateTextEdits(); 320 | const workspaceEdit = new WorkspaceEdit(); 321 | 322 | workspaceEdit.set(this.document.uri, edits); 323 | 324 | this.logger.debug('[ImportManager] Commit the file', { 325 | file: this.document.fileName, 326 | }); 327 | 328 | const result = await workspace.applyEdit(workspaceEdit); 329 | 330 | if (result) { 331 | delete this.organize; 332 | this._parsedDocument = await this.parser.parseSource( 333 | this.document.getText(), 334 | getScriptKind(this.document.fileName), 335 | ); 336 | this.imports = this._parsedDocument.imports.map(o => o.clone()); 337 | for (const group of this.importGroups) { 338 | group.reset(); 339 | } 340 | this.addImportsToGroups(this.imports); 341 | } 342 | 343 | return result; 344 | } 345 | 346 | /** 347 | * Calculate the needed {@link TextEdit} array for the actual changes in the imports. 348 | * 349 | * @returns {TextEdit[]} 350 | * 351 | * @memberof ImportManager 352 | */ 353 | public calculateTextEdits(): TextEdit[] { 354 | const edits: TextEdit[] = []; 355 | 356 | if (this.organize) { 357 | // since the imports should be organized: 358 | // delete all imports and the following lines (if empty) 359 | // newly generate all groups. 360 | for (const imp of this._parsedDocument.imports) { 361 | edits.push( 362 | TextEdit.delete(importRange(this.document, imp.start, imp.end)), 363 | ); 364 | if (imp.end !== undefined) { 365 | const nextLine = this.document.lineAt( 366 | this.document.positionAt(imp.end).line + 1, 367 | ); 368 | if (nextLine.text === '') { 369 | edits.push(TextEdit.delete(nextLine.rangeIncludingLineBreak)); 370 | } 371 | } 372 | } 373 | const imports = this.importGroups 374 | .map(group => this.generator.generate(group as any)) 375 | .filter(Boolean) 376 | .join('\n'); 377 | if (!!imports) { 378 | edits.push( 379 | TextEdit.insert( 380 | getImportInsertPosition(window.activeTextEditor), 381 | `${imports}\n`, 382 | ), 383 | ); 384 | } 385 | } else { 386 | // Commit the documents imports: 387 | // 1. Remove imports that are in the document, but not anymore 388 | // 2. Update existing / insert new ones 389 | for (const imp of this._parsedDocument.imports) { 390 | if (!this.imports.some(o => o.libraryName === imp.libraryName)) { 391 | edits.push( 392 | TextEdit.delete(importRange(this.document, imp.start, imp.end)), 393 | ); 394 | } 395 | } 396 | const actualDocumentsNamed = this._parsedDocument.imports.filter( 397 | o => o instanceof NamedImport, 398 | ) as NamedImport[]; 399 | for (const imp of this.imports) { 400 | if ( 401 | imp instanceof NamedImport && 402 | actualDocumentsNamed.some( 403 | o => 404 | o.libraryName === imp.libraryName && 405 | o.defaultAlias === imp.defaultAlias && 406 | o.specifiers.length === imp.specifiers.length && 407 | sameSpecifiers(o.specifiers, imp.specifiers), 408 | ) 409 | ) { 410 | continue; 411 | } 412 | if (imp.isNew) { 413 | edits.push( 414 | TextEdit.insert( 415 | getImportInsertPosition(window.activeTextEditor), 416 | this.generator.generate(imp) + '\n', 417 | ), 418 | ); 419 | } else { 420 | edits.push( 421 | TextEdit.replace( 422 | new Range( 423 | this.document.positionAt(imp.start!), 424 | this.document.positionAt(imp.end!), 425 | ), 426 | this.generator.generate(imp), 427 | ), 428 | ); 429 | } 430 | } 431 | } 432 | 433 | return edits; 434 | } 435 | 436 | /** 437 | * Add a list of imports to the groups of the ImportManager. 438 | * 439 | * @private 440 | * @param {Import[]} imports 441 | * 442 | * @memberof ImportManager 443 | */ 444 | private addImportsToGroups(imports: Import[]): void { 445 | const importGroupsWithPrecedence = importGroupSortForPrecedence( 446 | this.importGroups, 447 | ); 448 | for (const tsImport of imports) { 449 | for (const group of importGroupsWithPrecedence) { 450 | if (group.processImport(tsImport)) { 451 | break; 452 | } 453 | } 454 | } 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/imports/import-organizer.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify'; 2 | import { commands, ExtensionContext, TextDocumentWillSaveEvent, window, workspace } from 'vscode'; 3 | 4 | import { Activatable } from '../activatable'; 5 | import { Configuration } from '../configuration'; 6 | import { ImportManagerProvider, iocSymbols } from '../ioc-symbols'; 7 | import { Logger } from '../utilities/logger'; 8 | 9 | @injectable() 10 | export class ImportOrganizer implements Activatable { 11 | constructor( 12 | @inject(iocSymbols.extensionContext) private context: ExtensionContext, 13 | @inject(iocSymbols.logger) private logger: Logger, 14 | @inject(iocSymbols.configuration) private config: Configuration, 15 | @inject(iocSymbols.importManager) 16 | private importManagerProvider: ImportManagerProvider, 17 | ) {} 18 | 19 | public setup(): void { 20 | this.logger.debug('[ImportOrganizer] Setting up ImportOrganizer.'); 21 | this.context.subscriptions.push( 22 | commands.registerTextEditorCommand( 23 | 'typescriptHero.imports.organize', 24 | () => this.organizeImports(), 25 | ), 26 | ); 27 | this.context.subscriptions.push( 28 | workspace.onWillSaveTextDocument((event: TextDocumentWillSaveEvent) => { 29 | if (!this.config.imports.organizeOnSave(event.document.uri)) { 30 | this.logger.debug( 31 | '[ImportOrganizer] OrganizeOnSave is deactivated through config', 32 | ); 33 | return; 34 | } 35 | if ( 36 | this.config.parseableLanguages().indexOf(event.document.languageId) < 37 | 0 38 | ) { 39 | this.logger.debug( 40 | '[ImportOrganizer] OrganizeOnSave not possible for given language', 41 | { language: event.document.languageId }, 42 | ); 43 | return; 44 | } 45 | 46 | this.logger.info('[ImportOrganizer] OrganizeOnSave for file', { 47 | file: event.document.fileName, 48 | }); 49 | event.waitUntil( 50 | this.importManagerProvider(event.document).then(manager => 51 | manager.organizeImports().calculateTextEdits(), 52 | ), 53 | ); 54 | }), 55 | ); 56 | } 57 | 58 | public start(): void { 59 | this.logger.info('[ImportOrganizer] Starting up ImportOrganizer.'); 60 | } 61 | 62 | public stop(): void { 63 | this.logger.info('[ImportOrganizer] Stopping ImportOrganizer.'); 64 | } 65 | 66 | public dispose(): void { 67 | this.logger.debug('[ImportOrganizer] Disposing ImportOrganizer.'); 68 | } 69 | 70 | /** 71 | * Organizes the imports of the actual document. Sorts and formats them correctly. 72 | * 73 | * @private 74 | * @returns {Promise} 75 | * 76 | * @memberof ImportOrganizer 77 | */ 78 | private async organizeImports(): Promise { 79 | if (!window.activeTextEditor) { 80 | return; 81 | } 82 | try { 83 | this.logger.debug( 84 | '[ImportOrganizer] Organize the imports in the document', 85 | { file: window.activeTextEditor.document.fileName }, 86 | ); 87 | const ctrl = await this.importManagerProvider( 88 | window.activeTextEditor.document, 89 | ); 90 | await ctrl.organizeImports().commit(); 91 | } catch (e) { 92 | this.logger.error( 93 | '[ImportOrganizer] Imports could not be organized, error: %s', 94 | e, 95 | { file: window.activeTextEditor.document.fileName }, 96 | ); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/imports/index.ts: -------------------------------------------------------------------------------- 1 | export { ImportOrganizer } from './import-organizer'; 2 | export { ImportManager } from './import-manager'; 3 | -------------------------------------------------------------------------------- /src/ioc-symbols.ts: -------------------------------------------------------------------------------- 1 | import { TypescriptCodeGenerator } from 'typescript-parser'; 2 | import { TextDocument, Uri } from 'vscode'; 3 | 4 | import { ImportManager } from './imports/import-manager'; 5 | 6 | export const iocSymbols = { 7 | activatables: Symbol('activatables'), 8 | configuration: Symbol('configuration'), 9 | declarationManager: Symbol('declarationManager'), 10 | extensionContext: Symbol('extensionContext'), 11 | generatorFactory: Symbol('generatorFactory'), 12 | importManager: Symbol('importManager'), 13 | logger: Symbol('logger'), 14 | parser: Symbol('parser'), 15 | }; 16 | 17 | export type ImportManagerProvider = ( 18 | document: TextDocument, 19 | ) => Promise; 20 | export type TypescriptCodeGeneratorFactory = ( 21 | resource: Uri, 22 | ) => TypescriptCodeGenerator; 23 | -------------------------------------------------------------------------------- /src/ioc.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Container, interfaces } from 'inversify'; 4 | import { TypescriptCodeGenerator, TypescriptParser } from 'typescript-parser'; 5 | import { ExtensionContext, TextDocument, Uri } from 'vscode'; 6 | 7 | import { Activatable } from './activatable'; 8 | import { Configuration } from './configuration'; 9 | import { ImportManager, ImportOrganizer } from './imports'; 10 | import { ImportManagerProvider, iocSymbols, TypescriptCodeGeneratorFactory } from './ioc-symbols'; 11 | import { TypescriptHero } from './typescript-hero'; 12 | import { Logger, winstonLogger } from './utilities/logger'; 13 | import { getScriptKind } from './utilities/utility-functions'; 14 | 15 | const iocContainer = new Container(); 16 | 17 | // Entry point 18 | iocContainer 19 | .bind(TypescriptHero) 20 | .to(TypescriptHero) 21 | .inSingletonScope(); 22 | 23 | // Activatables 24 | iocContainer 25 | .bind(iocSymbols.activatables) 26 | .to(ImportOrganizer) 27 | .inSingletonScope(); 28 | 29 | // Configuration 30 | iocContainer 31 | .bind(iocSymbols.configuration) 32 | .to(Configuration) 33 | .inSingletonScope(); 34 | 35 | // Logging 36 | iocContainer 37 | .bind(iocSymbols.logger) 38 | .toDynamicValue((context: interfaces.Context) => { 39 | const extContext = context.container.get( 40 | iocSymbols.extensionContext, 41 | ); 42 | const config = context.container.get( 43 | iocSymbols.configuration, 44 | ); 45 | return winstonLogger(config.verbosity(), extContext); 46 | }) 47 | .inSingletonScope(); 48 | 49 | // Managers 50 | iocContainer 51 | .bind(iocSymbols.importManager) 52 | .toProvider(c => async (document: TextDocument) => { 53 | const parser = c.container.get(iocSymbols.parser); 54 | const config = c.container.get(iocSymbols.configuration); 55 | const logger = c.container.get(iocSymbols.logger); 56 | const generatorFactory = c.container.get( 57 | iocSymbols.generatorFactory, 58 | ); 59 | const source = await parser.parseSource( 60 | document.getText(), 61 | getScriptKind(document.fileName), 62 | ); 63 | return new ImportManager( 64 | document, 65 | source, 66 | parser, 67 | config, 68 | logger, 69 | generatorFactory, 70 | ); 71 | }); 72 | 73 | // Typescript 74 | iocContainer 75 | .bind(iocSymbols.parser) 76 | .toConstantValue(new TypescriptParser()); 77 | iocContainer 78 | .bind(iocSymbols.generatorFactory) 79 | .toFactory((context: interfaces.Context) => { 80 | return (resource: Uri) => { 81 | const config = context.container.get( 82 | iocSymbols.configuration, 83 | ); 84 | return new TypescriptCodeGenerator( 85 | config.typescriptGeneratorOptions(resource), 86 | ); 87 | }; 88 | }); 89 | 90 | export const ioc = iocContainer; 91 | -------------------------------------------------------------------------------- /src/typescript-hero.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable, multiInject } from 'inversify'; 2 | import { Generatable, GENERATORS, TypescriptCodeGenerator, TypescriptGenerationOptions } from 'typescript-parser'; 3 | 4 | import { Activatable } from './activatable'; 5 | import { KeywordImportGroup, RegexImportGroup, RemainImportGroup } from './imports/import-grouping'; 6 | import { iocSymbols } from './ioc-symbols'; 7 | import { Logger } from './utilities/logger'; 8 | 9 | @injectable() 10 | export class TypescriptHero implements Activatable { 11 | constructor( 12 | @inject(iocSymbols.logger) private logger: Logger, 13 | @multiInject(iocSymbols.activatables) private activatables: Activatable[], 14 | ) {} 15 | 16 | public setup(): void { 17 | this.logger.debug( 18 | '[TypescriptHero] Setting up extension and activatables.', 19 | ); 20 | this.extendCodeGenerator(); 21 | for (const activatable of this.activatables) { 22 | activatable.setup(); 23 | } 24 | } 25 | 26 | public start(): void { 27 | this.logger.debug( 28 | '[TypescriptHero] Starting up extension and activatables.', 29 | ); 30 | for (const activatable of this.activatables) { 31 | activatable.start(); 32 | } 33 | } 34 | 35 | public stop(): void { 36 | this.logger.debug('[TypescriptHero] Stopping extension and activatables.'); 37 | for (const activatable of this.activatables) { 38 | activatable.stop(); 39 | } 40 | } 41 | 42 | public dispose(): void { 43 | this.logger.debug('[TypescriptHero] Disposing extension and activatables.'); 44 | for (const activatable of this.activatables) { 45 | activatable.dispose(); 46 | } 47 | } 48 | 49 | private extendCodeGenerator(): void { 50 | function simpleGenerator( 51 | generatable: Generatable, 52 | options: TypescriptGenerationOptions, 53 | ): string { 54 | const gen = new TypescriptCodeGenerator(options); 55 | const group = generatable as KeywordImportGroup; 56 | if (!group.imports.length) { 57 | return ''; 58 | } 59 | return ( 60 | group.sortedImports.map(imp => gen.generate(imp)).join('\n') + '\n' 61 | ); 62 | } 63 | 64 | GENERATORS[KeywordImportGroup.name] = simpleGenerator; 65 | GENERATORS[RegexImportGroup.name] = simpleGenerator; 66 | GENERATORS[RemainImportGroup.name] = simpleGenerator; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/utilities/logger.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, OutputChannel, window } from 'vscode'; 2 | 3 | const { createLogger, exceptions, format, transports } = require('winston'); 4 | const transportClass = require('winston-transport'); 5 | const { LEVEL, MESSAGE } = require('triple-beam'); 6 | 7 | const { combine, timestamp, printf } = format; 8 | const transport = transportClass as { new (...args: any[]): any }; 9 | 10 | const levels = { 11 | error: 0, 12 | warn: 1, 13 | info: 2, 14 | debug: 3, 15 | }; 16 | 17 | class OutputWindowTransport extends transport { 18 | constructor(opts: any, private channel: OutputChannel) { 19 | super(opts); 20 | } 21 | 22 | public log(info: any, callback: any): void { 23 | setImmediate(() => { 24 | this.emit('logged', info); 25 | }); 26 | this.channel.appendLine(info[MESSAGE]); 27 | callback(); 28 | } 29 | } 30 | 31 | class ConsoleLogTransport extends transport { 32 | constructor(opts?: any) { 33 | super(opts); 34 | } 35 | 36 | public log(info: any, callback: any): void { 37 | setImmediate(() => { 38 | this.emit('logged', info); 39 | }); 40 | const level = info[LEVEL]; 41 | 42 | switch (level) { 43 | case 'error': 44 | console.error(info[MESSAGE]); 45 | break; 46 | case 'warn': 47 | console.warn(info[MESSAGE]); 48 | break; 49 | default: 50 | console.log(info[MESSAGE]); 51 | break; 52 | } 53 | callback(); 54 | } 55 | } 56 | 57 | export interface Logger { 58 | error: (message: string, ...data: any[]) => void; 59 | warn: (message: string, ...data: any[]) => void; 60 | info: (message: string, ...data: any[]) => void; 61 | debug: (message: string, ...data: any[]) => void; 62 | profile: (name: string) => void; 63 | startTimer(): { 64 | done: (info: { message: string; [key: string]: any }) => void; 65 | }; 66 | } 67 | 68 | const loggerTransports = [ 69 | new ConsoleLogTransport({ 70 | level: !!process.env.CI || !!process.env.LOCAL_TEST ? 'error' : 'debug', 71 | }), 72 | ]; 73 | 74 | export function winstonLogger( 75 | verbosity: keyof typeof levels, 76 | context: ExtensionContext, 77 | ): Logger { 78 | const level = !!process.env.CI ? 'error' : verbosity; 79 | 80 | if (!process.env.CI && !process.env.EXT_DEBUG && !process.env.LOCAL_TEST) { 81 | const channel = window.createOutputChannel('TypeScript Hero'); 82 | context.subscriptions.push(channel); 83 | 84 | const fileHandler = new transports.File({ 85 | level: ['info', 'debug'].indexOf(level) >= 0 ? level : 'info', 86 | exitOnError: false, 87 | filename: 'typescript-hero.log', 88 | dirname: context.extensionPath, 89 | maxsize: 1024 * 1024, 90 | maxFiles: 1, 91 | tailable: true, 92 | }); 93 | const outputHandler = new OutputWindowTransport( 94 | { exitOnError: false }, 95 | channel, 96 | ); 97 | 98 | loggerTransports.push(fileHandler); 99 | loggerTransports.push(outputHandler); 100 | 101 | exceptions.handle(fileHandler); 102 | } 103 | 104 | const logger = createLogger({ 105 | level, 106 | levels, 107 | format: combine( 108 | format.splat(), 109 | timestamp(), 110 | printf((info: any) => { 111 | const message = `${info.timestamp} - ${info.level}: ${info.message}`; 112 | const data = { 113 | ...info, 114 | level: undefined, 115 | message: undefined, 116 | splat: undefined, 117 | timestamp: undefined, 118 | }; 119 | if (Object.keys(data).filter(key => !!data[key]).length > 0) { 120 | return `${message} ${JSON.stringify(data)}`; 121 | } 122 | return message; 123 | }), 124 | ), 125 | transports: loggerTransports, 126 | }); 127 | 128 | logger.exitOnError = false; 129 | 130 | return logger; 131 | } 132 | -------------------------------------------------------------------------------- /src/utilities/utility-functions.ts: -------------------------------------------------------------------------------- 1 | import { basename, join, normalize, parse, relative } from 'path'; 2 | import { ScriptKind } from 'typescript'; 3 | import { 4 | ClassDeclaration, 5 | ConstructorDeclaration, 6 | Declaration, 7 | DeclarationInfo, 8 | DefaultDeclaration, 9 | EnumDeclaration, 10 | ExternalModuleImport, 11 | FunctionDeclaration, 12 | GetterDeclaration, 13 | Import, 14 | InterfaceDeclaration, 15 | MethodDeclaration, 16 | ModuleDeclaration, 17 | NamedImport, 18 | NamespaceImport, 19 | ParameterDeclaration, 20 | PropertyDeclaration, 21 | SetterDeclaration, 22 | StringImport, 23 | SymbolSpecifier, 24 | TypeAliasDeclaration, 25 | VariableDeclaration, 26 | } from 'typescript-parser'; 27 | import { toPosix } from 'typescript-parser/utilities/PathHelpers'; 28 | import { CompletionItemKind, Position, TextEditor } from 'vscode'; 29 | 30 | import { ImportGroup, RegexImportGroup } from '../imports/import-grouping'; 31 | 32 | /** 33 | * String-Sort function. 34 | * 35 | * @export 36 | * @param {string} strA 37 | * @param {string} strB 38 | * @param {('asc' | 'desc')} [order='asc'] 39 | * @returns {number} 40 | */ 41 | export function stringSort(strA: string, strB: string, order: 'asc' | 'desc' = 'asc'): number { 42 | let result: number = 0; 43 | if (strA < strB) { 44 | result = -1; 45 | } else if (strA > strB) { 46 | result = 1; 47 | } 48 | if (order === 'desc') { 49 | result *= -1; 50 | } 51 | return result; 52 | } 53 | 54 | /** 55 | * Orders import groups by matching precedence (regex first). This is used internally by 56 | * `ImportManager` when assigning imports to groups, so regex groups can appear later than 57 | * keyword groups yet capture relevant imports nonetheless. 58 | * 59 | * @export 60 | * @param {ImportGroup[]} importGroups The original import groups (as per extension configuration) 61 | * @returns {ImportGroup[]} The same list, with Regex import groups appearing first. 62 | */ 63 | export function importGroupSortForPrecedence(importGroups: ImportGroup[]): ImportGroup[] { 64 | const regexGroups: ImportGroup[] = []; 65 | const otherGroups: ImportGroup[] = []; 66 | for (const ig of importGroups) { 67 | (ig instanceof RegexImportGroup ? regexGroups : otherGroups).push(ig); 68 | } 69 | return regexGroups.concat(otherGroups); 70 | } 71 | 72 | /** 73 | * Locale-sensitive ("Human-compatible") String-Sort function. 74 | * 75 | * @param {string} strA 76 | * @param {string} strB 77 | * @param {('asc' | 'desc')} [order='asc'] 78 | * @returns {number} 79 | */ 80 | function localeStringSort(strA: string, strB: string, order: 'asc' | 'desc' = 'asc'): number { 81 | let result: number = strA.localeCompare(strB); 82 | if (order === 'desc') { 83 | result *= -1; 84 | } 85 | return result; 86 | } 87 | 88 | /** 89 | * Order imports by library name. 90 | * 91 | * @export 92 | * @param {Import} i1 93 | * @param {Import} i2 94 | * @param {('asc' | 'desc')} [order='asc'] 95 | * @returns {number} 96 | */ 97 | export function importSort(i1: Import, i2: Import, order: 'asc' | 'desc' = 'asc'): number { 98 | const strA = i1.libraryName.toLowerCase(); 99 | const strB = i2.libraryName.toLowerCase(); 100 | 101 | return stringSort(strA, strB, order); 102 | } 103 | 104 | /** 105 | * Order imports by first specifier name. Does not re-sort specifiers internally: 106 | * assumes they were sorted AOT (which happens in `ImportManager#organizeImports`, 107 | * indeed). 108 | * 109 | * @export 110 | * @param {Import} i1 111 | * @param {Import} i2 112 | * @param {('asc' | 'desc')} [order='asc'] 113 | * @returns {number} 114 | */ 115 | export function importSortByFirstSpecifier(i1: Import, i2: Import, order: 'asc' | 'desc' = 'asc'): number { 116 | const strA = getImportFirstSpecifier(i1); 117 | const strB = getImportFirstSpecifier(i2); 118 | 119 | return localeStringSort(strA, strB, order); 120 | } 121 | 122 | /** 123 | * Computes the first specifier/alias of an import, falling back ot its 124 | * module path (for StringImports, basically). Does not re-sort specifiers 125 | * internally: assumes they were sorted AOT (which happens in 126 | * `ImportManager#organizeImports`, indeed). 127 | * 128 | * @param {Import} imp 129 | * @returns {String} 130 | */ 131 | function getImportFirstSpecifier(imp: Import): string { 132 | if (imp instanceof NamespaceImport || imp instanceof ExternalModuleImport) { 133 | return imp.alias; 134 | } 135 | 136 | if (imp instanceof StringImport) { 137 | return basename(imp.libraryName); 138 | } 139 | 140 | if (imp instanceof NamedImport) { 141 | const namedSpecifiers = (imp as NamedImport).specifiers 142 | .map(s => s.alias || s.specifier) 143 | .filter(Boolean); 144 | const marker = namedSpecifiers[0] || imp.defaultAlias; 145 | if (marker) { 146 | return marker; 147 | } 148 | } 149 | 150 | return basename(imp.libraryName); 151 | } 152 | 153 | /** 154 | * Order specifiers by name. 155 | * 156 | * @export 157 | * @param {SymbolSpecifier} i1 158 | * @param {SymbolSpecifier} i2 159 | * @returns {number} 160 | */ 161 | export function specifierSort(i1: SymbolSpecifier, i2: SymbolSpecifier): number { 162 | return stringSort(i1.specifier, i2.specifier); 163 | } 164 | 165 | /** 166 | * Returns the item kind for a given declaration. 167 | * 168 | * @export 169 | * @param {Declaration} declaration 170 | * @returns {CompletionItemKind} 171 | */ 172 | export function getItemKind(declaration: Declaration): CompletionItemKind { 173 | switch (true) { 174 | case declaration instanceof ClassDeclaration: 175 | return CompletionItemKind.Class; 176 | case declaration instanceof ConstructorDeclaration: 177 | return CompletionItemKind.Constructor; 178 | case declaration instanceof DefaultDeclaration: 179 | return CompletionItemKind.File; 180 | case declaration instanceof EnumDeclaration: 181 | return CompletionItemKind.Enum; 182 | case declaration instanceof FunctionDeclaration: 183 | return CompletionItemKind.Function; 184 | case declaration instanceof InterfaceDeclaration: 185 | return CompletionItemKind.Interface; 186 | case declaration instanceof MethodDeclaration: 187 | return CompletionItemKind.Method; 188 | case declaration instanceof ModuleDeclaration: 189 | return CompletionItemKind.Module; 190 | case declaration instanceof ParameterDeclaration: 191 | return CompletionItemKind.Variable; 192 | case declaration instanceof PropertyDeclaration: 193 | return CompletionItemKind.Property; 194 | case declaration instanceof TypeAliasDeclaration: 195 | return CompletionItemKind.TypeParameter; 196 | case declaration instanceof VariableDeclaration: 197 | const variable = declaration as VariableDeclaration; 198 | return variable.isConst ? 199 | CompletionItemKind.Constant : 200 | CompletionItemKind.Variable; 201 | case declaration instanceof GetterDeclaration: 202 | case declaration instanceof SetterDeclaration: 203 | return CompletionItemKind.Method; 204 | default: 205 | return CompletionItemKind.Reference; 206 | } 207 | } 208 | 209 | /** 210 | * Calculates the scriptkind for the typescript parser based on filepath. 211 | * 212 | * @export 213 | * @param {string} filePath 214 | * @returns {ScriptKind} 215 | */ 216 | export function getScriptKind(filePath: string | undefined): ScriptKind { 217 | if (!filePath) { 218 | return ScriptKind.TS; 219 | } 220 | const parsed = parse(filePath); 221 | switch (parsed.ext) { 222 | case '.ts': 223 | return ScriptKind.TS; 224 | case '.tsx': 225 | return ScriptKind.TSX; 226 | case '.js': 227 | return ScriptKind.JS; 228 | case '.jsx': 229 | return ScriptKind.JSX; 230 | default: 231 | return ScriptKind.Unknown; 232 | } 233 | } 234 | 235 | /** 236 | * Calculates a list of declarationInfos filtered by the already imported ones in the given document. 237 | * The result is a list of declarations that are not already imported by the document. 238 | * 239 | * @export 240 | * @param {ResolveIndex} resolveIndex 241 | * @param {string} documentPath 242 | * @param {TsImport[]} imports 243 | * @param {string} [rootPath] 244 | * @returns {DeclarationInfo[]} 245 | */ 246 | export function getDeclarationsFilteredByImports( 247 | declarationInfos: DeclarationInfo[], 248 | documentPath: string, 249 | imports: Import[], 250 | rootPath?: string, 251 | ): DeclarationInfo[] { 252 | let declarations = declarationInfos; 253 | 254 | for (const tsImport of imports) { 255 | const importedLib = getAbsolutLibraryName(tsImport.libraryName, documentPath, rootPath); 256 | 257 | if (tsImport instanceof NamedImport) { 258 | declarations = declarations.filter( 259 | d => d.from !== importedLib || 260 | !tsImport.specifiers.some(s => s.specifier === d.declaration.name), 261 | ); 262 | if (tsImport.defaultAlias) { 263 | declarations = declarations.filter( 264 | d => !(tsImport.defaultAlias && d.declaration instanceof DefaultDeclaration && d.from === importedLib), 265 | ); 266 | } 267 | } else if (tsImport instanceof NamespaceImport || tsImport instanceof ExternalModuleImport) { 268 | declarations = declarations.filter(o => o.from !== tsImport.libraryName); 269 | } 270 | } 271 | 272 | return declarations; 273 | } 274 | 275 | /** 276 | * Returns the absolut workspace specific library path. 277 | * If the library is a node module or a typings module, the name 278 | * is returned. If the "lib" is in the local workspace, then the 279 | * absolut path from the workspace root is returned. 280 | * 281 | * @param {string} library Name of the library 282 | * @param {string} actualFilePath Filepath of the actually open file 283 | * @param {string} [rootPath] Root path of the workspace 284 | * @returns {string} Absolut path from the workspace root to the desired library 285 | */ 286 | export function getAbsolutLibraryName(library: string, actualFilePath: string, rootPath?: string): string { 287 | if (!library.startsWith('.') || !rootPath) { 288 | return library; 289 | } 290 | return '/' + toPosix(relative( 291 | rootPath, 292 | normalize(join(parse(actualFilePath).dir, library)), 293 | )).replace(/\/$/, ''); 294 | } 295 | 296 | /** 297 | * Returns the relative path to a specific library. 298 | * If the library is a node module or a typings module, the name 299 | * is returned. If the "lib" is in the local workspace, then the 300 | * relative path from the actual file is returned. 301 | * 302 | * @param {string} library Name of the library 303 | * @param {string} actualFilePath Filepath of the actually open file 304 | * @param {string} [rootPath] Root path of the workspace 305 | * @returns {string} Relative path from the actual file to the library 306 | */ 307 | export function getRelativeLibraryName(library: string, actualFilePath: string, rootPath?: string): string { 308 | if (!library.startsWith('/') || !rootPath) { 309 | return library; 310 | } 311 | 312 | const actualDir = parse('/' + relative(rootPath, actualFilePath)).dir; 313 | let relativePath = relative(actualDir, library); 314 | 315 | if (!relativePath.startsWith('.')) { 316 | relativePath = './' + relativePath; 317 | } else if (relativePath === '..') { 318 | relativePath += '/'; 319 | } 320 | return toPosix(relativePath); 321 | } 322 | 323 | const REGEX_IGNORED_LINE = /^\s*(?:\/\/|\/\*|\*\/|\*|#!|(['"])use strict\1)/; 324 | 325 | /** 326 | * Calculate the position, where a new import should be inserted. 327 | * Does respect the "use strict" string as first line of a document. 328 | * 329 | * @export 330 | * @param {TextEditor | undefined} editor 331 | * @returns {Position} 332 | */ 333 | export function getImportInsertPosition(editor: TextEditor | undefined): Position { 334 | if (!editor) { 335 | return new Position(0, 0); 336 | } 337 | const lines = editor.document.getText().split('\n'); 338 | const index = lines.findIndex(line => !REGEX_IGNORED_LINE.test(line)); 339 | return new Position(Math.max(0, index), 0); 340 | } 341 | -------------------------------------------------------------------------------- /test/imports/__snapshots__/import-manager.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ImportManager Typescript Constructor (via provider) should add an import proxy for a default import 1`] = ` 4 | Array [ 5 | NamedImport { 6 | "defaultAlias": "myDefaultExportedFunction", 7 | "end": 84, 8 | "libraryName": "../defaultExport/lateDefaultExportedElement", 9 | "specifiers": Array [], 10 | "start": 0, 11 | }, 12 | ] 13 | `; 14 | 15 | exports[`ImportManager Typescript Constructor (via provider) should add an import proxy for a named import 1`] = ` 16 | Array [ 17 | NamedImport { 18 | "end": 43, 19 | "libraryName": "../server/indices", 20 | "specifiers": Array [ 21 | SymbolSpecifier { 22 | "alias": undefined, 23 | "specifier": "Class1", 24 | }, 25 | ], 26 | "start": 0, 27 | }, 28 | ] 29 | `; 30 | 31 | exports[`ImportManager Typescript Constructor (via provider) should add multiple import proxies 1`] = ` 32 | Array [ 33 | NamedImport { 34 | "defaultAlias": "myDefaultExportedFunction", 35 | "end": 84, 36 | "libraryName": "../defaultExport/lateDefaultExportedElement", 37 | "specifiers": Array [], 38 | "start": 0, 39 | }, 40 | NamedImport { 41 | "end": 128, 42 | "libraryName": "../server/indices", 43 | "specifiers": Array [ 44 | SymbolSpecifier { 45 | "alias": undefined, 46 | "specifier": "Class1", 47 | }, 48 | ], 49 | "start": 85, 50 | }, 51 | ] 52 | `; 53 | 54 | exports[`ImportManager Typescript Constructor (via provider) should not add a proxy for a namespace import 1`] = ` 55 | Array [ 56 | NamespaceImport { 57 | "alias": "bodyParser", 58 | "end": 42, 59 | "libraryName": "body-parser", 60 | "start": 0, 61 | }, 62 | ] 63 | `; 64 | 65 | exports[`ImportManager Typescript Constructor (via provider) should not add a proxy for a string import 1`] = ` 66 | Array [ 67 | StringImport { 68 | "end": 21, 69 | "libraryName": "body-parser", 70 | "start": 0, 71 | }, 72 | ] 73 | `; 74 | 75 | exports[`ImportManager Typescript Constructor (via provider) should not add a proxy for an external import 1`] = ` 76 | Array [ 77 | ExternalModuleImport { 78 | "alias": "bodyParser", 79 | "end": 43, 80 | "libraryName": "body-parser", 81 | "start": 0, 82 | }, 83 | ] 84 | `; 85 | 86 | exports[`ImportManager Typescript addDeclarationImport() should add a default import to the import index. 1`] = ` 87 | Array [ 88 | NamedImport { 89 | "defaultAlias": undefined, 90 | "end": 43, 91 | "libraryName": "../server/indices", 92 | "specifiers": Array [ 93 | SymbolSpecifier { 94 | "alias": undefined, 95 | "specifier": "Class1", 96 | }, 97 | ], 98 | "start": 0, 99 | }, 100 | NamedImport { 101 | "defaultAlias": "defaultdeclaration", 102 | "end": undefined, 103 | "libraryName": "../lib", 104 | "specifiers": Array [], 105 | "start": undefined, 106 | }, 107 | ] 108 | `; 109 | 110 | exports[`ImportManager Typescript addDeclarationImport() should add a module import to the import index 1`] = ` 111 | Array [ 112 | NamedImport { 113 | "defaultAlias": undefined, 114 | "end": 43, 115 | "libraryName": "../server/indices", 116 | "specifiers": Array [ 117 | SymbolSpecifier { 118 | "alias": undefined, 119 | "specifier": "Class1", 120 | }, 121 | ], 122 | "start": 0, 123 | }, 124 | NamespaceImport { 125 | "alias": "module", 126 | "end": undefined, 127 | "libraryName": "my-module", 128 | "start": undefined, 129 | }, 130 | ] 131 | `; 132 | 133 | exports[`ImportManager Typescript addDeclarationImport() should add a normal import to a group 1`] = ` 134 | KeywordImportGroup { 135 | "imports": Array [ 136 | NamedImport { 137 | "defaultAlias": undefined, 138 | "end": 43, 139 | "libraryName": "../server/indices", 140 | "specifiers": Array [ 141 | SymbolSpecifier { 142 | "alias": undefined, 143 | "specifier": "Class1", 144 | }, 145 | ], 146 | "start": 0, 147 | }, 148 | ], 149 | "keyword": 2, 150 | "order": "asc", 151 | } 152 | `; 153 | 154 | exports[`ImportManager Typescript addDeclarationImport() should add a normal import to a group 2`] = ` 155 | KeywordImportGroup { 156 | "imports": Array [ 157 | NamedImport { 158 | "defaultAlias": undefined, 159 | "end": 43, 160 | "libraryName": "../server/indices", 161 | "specifiers": Array [ 162 | SymbolSpecifier { 163 | "alias": undefined, 164 | "specifier": "Class1", 165 | }, 166 | ], 167 | "start": 0, 168 | }, 169 | NamedImport { 170 | "end": undefined, 171 | "libraryName": "../lib", 172 | "specifiers": Array [ 173 | SymbolSpecifier { 174 | "alias": undefined, 175 | "specifier": "class", 176 | }, 177 | ], 178 | "start": undefined, 179 | }, 180 | ], 181 | "keyword": 2, 182 | "order": "asc", 183 | } 184 | `; 185 | 186 | exports[`ImportManager Typescript addDeclarationImport() should add a normal import to the document 1`] = ` 187 | Array [ 188 | NamedImport { 189 | "defaultAlias": undefined, 190 | "end": 43, 191 | "libraryName": "../server/indices", 192 | "specifiers": Array [ 193 | SymbolSpecifier { 194 | "alias": undefined, 195 | "specifier": "Class1", 196 | }, 197 | ], 198 | "start": 0, 199 | }, 200 | NamedImport { 201 | "end": undefined, 202 | "libraryName": "../lib", 203 | "specifiers": Array [ 204 | SymbolSpecifier { 205 | "alias": undefined, 206 | "specifier": "classdeclaration", 207 | }, 208 | ], 209 | "start": undefined, 210 | }, 211 | ] 212 | `; 213 | 214 | exports[`ImportManager Typescript addDeclarationImport() should add an import to an existing import group 1`] = ` 215 | KeywordImportGroup { 216 | "imports": Array [ 217 | NamedImport { 218 | "defaultAlias": undefined, 219 | "end": 43, 220 | "libraryName": "../server/indices", 221 | "specifiers": Array [ 222 | SymbolSpecifier { 223 | "alias": undefined, 224 | "specifier": "Class1", 225 | }, 226 | ], 227 | "start": 0, 228 | }, 229 | ], 230 | "keyword": 2, 231 | "order": "asc", 232 | } 233 | `; 234 | 235 | exports[`ImportManager Typescript addDeclarationImport() should add an import to an existing import group 2`] = ` 236 | KeywordImportGroup { 237 | "imports": Array [ 238 | NamedImport { 239 | "defaultAlias": undefined, 240 | "end": 43, 241 | "libraryName": "../server/indices", 242 | "specifiers": Array [ 243 | SymbolSpecifier { 244 | "alias": undefined, 245 | "specifier": "Class1", 246 | }, 247 | ], 248 | "start": 0, 249 | }, 250 | NamedImport { 251 | "end": undefined, 252 | "libraryName": "../lib1", 253 | "specifiers": Array [ 254 | SymbolSpecifier { 255 | "alias": undefined, 256 | "specifier": "class", 257 | }, 258 | ], 259 | "start": undefined, 260 | }, 261 | NamedImport { 262 | "end": undefined, 263 | "libraryName": "../lib1", 264 | "specifiers": Array [ 265 | SymbolSpecifier { 266 | "alias": undefined, 267 | "specifier": "class2", 268 | }, 269 | ], 270 | "start": undefined, 271 | }, 272 | ], 273 | "keyword": 2, 274 | "order": "asc", 275 | } 276 | `; 277 | 278 | exports[`ImportManager Typescript addDeclarationImport() should add an import to an existing import index item 1`] = ` 279 | Array [ 280 | NamedImport { 281 | "defaultAlias": undefined, 282 | "end": 43, 283 | "libraryName": "../server/indices", 284 | "specifiers": Array [ 285 | SymbolSpecifier { 286 | "alias": undefined, 287 | "specifier": "Class1", 288 | }, 289 | ], 290 | "start": 0, 291 | }, 292 | NamedImport { 293 | "end": undefined, 294 | "libraryName": "../lib1", 295 | "specifiers": Array [ 296 | SymbolSpecifier { 297 | "alias": undefined, 298 | "specifier": "class1", 299 | }, 300 | ], 301 | "start": undefined, 302 | }, 303 | NamedImport { 304 | "end": undefined, 305 | "libraryName": "../lib1", 306 | "specifiers": Array [ 307 | SymbolSpecifier { 308 | "alias": undefined, 309 | "specifier": "class2", 310 | }, 311 | ], 312 | "start": undefined, 313 | }, 314 | ] 315 | `; 316 | 317 | exports[`ImportManager Typescript addDeclarationImport() should add multiple imports to the import index 1`] = ` 318 | Array [ 319 | NamedImport { 320 | "defaultAlias": undefined, 321 | "end": 43, 322 | "libraryName": "../server/indices", 323 | "specifiers": Array [ 324 | SymbolSpecifier { 325 | "alias": undefined, 326 | "specifier": "Class1", 327 | }, 328 | ], 329 | "start": 0, 330 | }, 331 | NamedImport { 332 | "end": undefined, 333 | "libraryName": "../lib1", 334 | "specifiers": Array [ 335 | SymbolSpecifier { 336 | "alias": undefined, 337 | "specifier": "class1", 338 | }, 339 | ], 340 | "start": undefined, 341 | }, 342 | NamedImport { 343 | "end": undefined, 344 | "libraryName": "../lib2", 345 | "specifiers": Array [ 346 | SymbolSpecifier { 347 | "alias": undefined, 348 | "specifier": "class2", 349 | }, 350 | ], 351 | "start": undefined, 352 | }, 353 | ] 354 | `; 355 | 356 | exports[`ImportManager Typescript addDeclarationImport() should not add the same specifier twice 1`] = ` 357 | Array [ 358 | NamedImport { 359 | "defaultAlias": undefined, 360 | "end": 43, 361 | "libraryName": "../server/indices", 362 | "specifiers": Array [ 363 | SymbolSpecifier { 364 | "alias": undefined, 365 | "specifier": "Class1", 366 | }, 367 | ], 368 | "start": 0, 369 | }, 370 | NamedImport { 371 | "end": undefined, 372 | "libraryName": "../lib", 373 | "specifiers": Array [ 374 | SymbolSpecifier { 375 | "alias": undefined, 376 | "specifier": "class", 377 | }, 378 | ], 379 | "start": undefined, 380 | }, 381 | ] 382 | `; 383 | 384 | exports[`ImportManager Typescript addDeclarationImport() should not add the same specifier twice 2`] = ` 385 | Array [ 386 | NamedImport { 387 | "defaultAlias": undefined, 388 | "end": 43, 389 | "libraryName": "../server/indices", 390 | "specifiers": Array [ 391 | SymbolSpecifier { 392 | "alias": undefined, 393 | "specifier": "Class1", 394 | }, 395 | ], 396 | "start": 0, 397 | }, 398 | NamedImport { 399 | "end": undefined, 400 | "libraryName": "../lib", 401 | "specifiers": Array [ 402 | SymbolSpecifier { 403 | "alias": undefined, 404 | "specifier": "class", 405 | }, 406 | ], 407 | "start": undefined, 408 | }, 409 | NamedImport { 410 | "end": undefined, 411 | "libraryName": "../lib", 412 | "specifiers": Array [ 413 | SymbolSpecifier { 414 | "alias": undefined, 415 | "specifier": "class", 416 | }, 417 | ], 418 | "start": undefined, 419 | }, 420 | ] 421 | `; 422 | 423 | exports[`ImportManager Typescript commit() should add a single default import to the document top 1`] = `"import defaultDeclaration from '../lib';"`; 424 | 425 | exports[`ImportManager Typescript commit() should add a single new import to the document top 1`] = `"import { importedclass } from '../imp';"`; 426 | 427 | exports[`ImportManager Typescript commit() should add a single new module import to the document top 1`] = `"import * as newModule from 'new-module';"`; 428 | 429 | exports[`ImportManager Typescript commit() should add a specifier to an existing import 1`] = `"import { Class1, class2 } from '../server/indices';"`; 430 | 431 | exports[`ImportManager Typescript commit() should add a specifier to an import and a new import 1`] = ` 432 | "import { class3 } from '../server/not-indices'; 433 | import { Class1, class2 } from '../server/indices'; 434 | " 435 | `; 436 | 437 | exports[`ImportManager Typescript commit() should add a specifier with a default (first) and a normal (second) import to the doc 1`] = `"import defaultExport, { defaultClass } from '../lib';"`; 438 | 439 | exports[`ImportManager Typescript commit() should add multiple specifier to an existing import 1`] = `"import { Class1, class2, class3 } from '../server/indices';"`; 440 | 441 | exports[`ImportManager Typescript commit() should add three new imports to the document top 1`] = ` 442 | "import { newClass } from '../lib'; 443 | import { secondnewClass } from '../not-same-lib'; 444 | import { thirdnewClass } from '../not-same-lib-again'; 445 | " 446 | `; 447 | 448 | exports[`ImportManager Typescript commit() should add two new imports to the document top 1`] = ` 449 | "import { newClass } from '../lib'; 450 | import { secondnewClass } from '../not-same-lib'; 451 | " 452 | `; 453 | 454 | exports[`ImportManager Typescript commit() should convert a default import when a normal specifier is added 1`] = `"import defaultExport, { Class1 } from '../server/indices';"`; 455 | 456 | exports[`ImportManager Typescript commit() should convert a default import when a normal specifier is added 2`] = `"import defaultExport, { Class1, defaultClass } from '../server/indices';"`; 457 | 458 | exports[`ImportManager Typescript commit() should convert a normal import when a default specifier is added 1`] = `"import { Class1, defaultClass } from '../server/indices';"`; 459 | 460 | exports[`ImportManager Typescript commit() should convert a normal import when a default specifier is added 2`] = `"import defaultExport, { Class1, defaultClass } from '../server/indices';"`; 461 | 462 | exports[`ImportManager Typescript commit() should render sorted imports when optimizing 1`] = ` 463 | "import { Class1 } from '../server/indices'; 464 | import { MultiExportClass } from '../server/indices/defaultExport/multiExport'; 465 | " 466 | `; 467 | 468 | exports[`ImportManager Typescript commit() should render sorted specifiers when optimizing 1`] = `"import { Class1, Class2 } from '../server/indices';"`; 469 | 470 | exports[`ImportManager Typescript commit() should render the optimized import 1`] = ` 471 | "import { SomeOtherClass } from '../server/not-indices'; 472 | import { Class1, Class2 } from '../server/indices'; 473 | " 474 | `; 475 | 476 | exports[`ImportManager Typescript commit() should render the optimized import 2`] = ` 477 | "import { Class1, Class2 } from '../server/indices'; 478 | 479 | " 480 | `; 481 | -------------------------------------------------------------------------------- /test/imports/__snapshots__/import-organizer.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ImportOrganizer import-organizer-file.ts should not remove default exported default imports 1`] = ` 4 | "import Barbaz from './foo'; 5 | 6 | export default Barbaz; 7 | " 8 | `; 9 | 10 | exports[`ImportOrganizer import-organizer-file.ts should not remove default exported default imports 2`] = ` 11 | "import Barbaz from './foo'; 12 | 13 | export default Barbaz; 14 | " 15 | `; 16 | 17 | exports[`ImportOrganizer import-organizer-file.ts should not remove directly exported default imports 1`] = ` 18 | "import Barbaz from './foo'; 19 | 20 | export { Barbaz } 21 | " 22 | `; 23 | 24 | exports[`ImportOrganizer import-organizer-file.ts should not remove directly exported default imports 2`] = ` 25 | "import Barbaz from './foo'; 26 | 27 | export { Barbaz } 28 | " 29 | `; 30 | 31 | exports[`ImportOrganizer import-organizer-file.ts should not remove directly exported imports 1`] = ` 32 | "import * as Foobar from './lol'; 33 | import * as Barbaz from './foo'; 34 | 35 | export { Foobar, Barbaz } 36 | " 37 | `; 38 | 39 | exports[`ImportOrganizer import-organizer-file.ts should not remove directly exported imports 2`] = ` 40 | "import * as Barbaz from './foo'; 41 | import * as Foobar from './lol'; 42 | 43 | export { Foobar, Barbaz } 44 | " 45 | `; 46 | 47 | exports[`ImportOrganizer import-organizer-file.tsx should not remove function that is used in tsx 1`] = ` 48 | "import { f } from './somewhere'; 49 | import * as React from 'react'; 50 | 51 | export class Comp extends React.Component { 52 | render() { 53 | return ( 54 |
{f()}
55 | ); 56 | } 57 | } 58 | " 59 | `; 60 | 61 | exports[`ImportOrganizer import-organizer-file.tsx should not remove function that is used in tsx 2`] = ` 62 | "import * as React from 'react'; 63 | 64 | import { f } from './somewhere'; 65 | 66 | export class Comp extends React.Component { 67 | render() { 68 | return ( 69 |
{f()}
70 | ); 71 | } 72 | } 73 | " 74 | `; 75 | -------------------------------------------------------------------------------- /test/imports/import-grouping/__snapshots__/import-group-setting-parser.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ImportGroupSettingParser should parse a complex keyword pattern 1`] = ` 4 | KeywordImportGroup { 5 | "imports": Array [], 6 | "keyword": 2, 7 | "order": "desc", 8 | } 9 | `; 10 | 11 | exports[`ImportGroupSettingParser should parse a complex regex 1`] = ` 12 | RegexImportGroup { 13 | "imports": Array [], 14 | "order": "asc", 15 | "regex": "/(@angular|react)/core/(.*)/", 16 | } 17 | `; 18 | 19 | exports[`ImportGroupSettingParser should parse a complex regex pattern 1`] = ` 20 | RegexImportGroup { 21 | "imports": Array [], 22 | "order": "desc", 23 | "regex": "/foobar/", 24 | } 25 | `; 26 | 27 | exports[`ImportGroupSettingParser should parse a regex with "@" 1`] = ` 28 | RegexImportGroup { 29 | "imports": Array [], 30 | "order": "asc", 31 | "regex": "/@angular/", 32 | } 33 | `; 34 | 35 | exports[`ImportGroupSettingParser should parse a regex with "or" 1`] = ` 36 | RegexImportGroup { 37 | "imports": Array [], 38 | "order": "asc", 39 | "regex": "/angular|react/", 40 | } 41 | `; 42 | 43 | exports[`ImportGroupSettingParser should parse a simple keyword 1`] = ` 44 | KeywordImportGroup { 45 | "imports": Array [], 46 | "keyword": 2, 47 | "order": "asc", 48 | } 49 | `; 50 | 51 | exports[`ImportGroupSettingParser should parse a simple regex 1`] = ` 52 | RegexImportGroup { 53 | "imports": Array [], 54 | "order": "asc", 55 | "regex": "/foobar/", 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /test/imports/import-grouping/__snapshots__/keyword-import-group.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`KeywordImportGroup keyword "Modules" should correctly process a list of imports 1`] = ` 4 | Array [ 5 | false, 6 | false, 7 | false, 8 | false, 9 | true, 10 | true, 11 | ] 12 | `; 13 | 14 | exports[`KeywordImportGroup keyword "Modules" should generate the correct typescript (asc) 1`] = ` 15 | "import { AnotherModuleFoo } from 'anotherLib'; 16 | import { ModuleFoobar } from 'myLib'; 17 | " 18 | `; 19 | 20 | exports[`KeywordImportGroup keyword "Modules" should generate the correct typescript (desc) 1`] = ` 21 | "import { ModuleFoobar } from 'myLib'; 22 | import { AnotherModuleFoo } from 'anotherLib'; 23 | " 24 | `; 25 | 26 | exports[`KeywordImportGroup keyword "Plains" should correctly process a list of imports 1`] = ` 27 | Array [ 28 | true, 29 | true, 30 | false, 31 | false, 32 | false, 33 | false, 34 | ] 35 | `; 36 | 37 | exports[`KeywordImportGroup keyword "Plains" should generate the correct typescript (asc) 1`] = ` 38 | "import './workspaceSideEffectLib'; 39 | import 'sideEffectLib'; 40 | " 41 | `; 42 | 43 | exports[`KeywordImportGroup keyword "Plains" should generate the correct typescript (desc) 1`] = ` 44 | "import 'sideEffectLib'; 45 | import './workspaceSideEffectLib'; 46 | " 47 | `; 48 | 49 | exports[`KeywordImportGroup keyword "Workspace" should correctly process a list of imports 1`] = ` 50 | Array [ 51 | false, 52 | false, 53 | true, 54 | true, 55 | false, 56 | false, 57 | ] 58 | `; 59 | 60 | exports[`KeywordImportGroup keyword "Workspace" should generate the correct typescript (asc) 1`] = ` 61 | "import { AnotherFoobar } from './anotherFile'; 62 | import { Foobar } from './myFile'; 63 | " 64 | `; 65 | 66 | exports[`KeywordImportGroup keyword "Workspace" should generate the correct typescript (desc) 1`] = ` 67 | "import { Foobar } from './myFile'; 68 | import { AnotherFoobar } from './anotherFile'; 69 | " 70 | `; 71 | -------------------------------------------------------------------------------- /test/imports/import-grouping/__snapshots__/regex-import-group.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RegexImportGroup should correctly process a list of imports 1`] = ` 4 | Array [ 5 | true, 6 | true, 7 | false, 8 | false, 9 | true, 10 | true, 11 | ] 12 | `; 13 | 14 | exports[`RegexImportGroup should generate the correct typescript (asc) 1`] = ` 15 | "import './workspaceSideEffectLib'; 16 | import 'sideEffectLib'; 17 | import { AnotherModuleFoo } from 'anotherLib'; 18 | import { ModuleFoobar } from 'myLib'; 19 | " 20 | `; 21 | 22 | exports[`RegexImportGroup should generate the correct typescript (desc) 1`] = ` 23 | "import 'sideEffectLib'; 24 | import './workspaceSideEffectLib'; 25 | import { ModuleFoobar } from 'myLib'; 26 | import { AnotherModuleFoo } from 'anotherLib'; 27 | " 28 | `; 29 | -------------------------------------------------------------------------------- /test/imports/import-grouping/__snapshots__/remain-import-group.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RemainImportGroup should generate the correct typescript (asc) 1`] = ` 4 | "import './workspaceSideEffectLib'; 5 | import 'sideEffectLib'; 6 | import { AnotherFoobar } from './anotherFile'; 7 | import { Foobar } from './myFile'; 8 | import { AnotherModuleFoo } from 'anotherLib'; 9 | import { ModuleFoobar } from 'myLib'; 10 | " 11 | `; 12 | 13 | exports[`RemainImportGroup should generate the correct typescript (desc) 1`] = ` 14 | "import 'sideEffectLib'; 15 | import './workspaceSideEffectLib'; 16 | import { ModuleFoobar } from 'myLib'; 17 | import { AnotherModuleFoo } from 'anotherLib'; 18 | import { Foobar } from './myFile'; 19 | import { AnotherFoobar } from './anotherFile'; 20 | " 21 | `; 22 | 23 | exports[`RemainImportGroup should process all imports 1`] = ` 24 | Array [ 25 | true, 26 | true, 27 | true, 28 | true, 29 | true, 30 | true, 31 | ] 32 | `; 33 | -------------------------------------------------------------------------------- /test/imports/import-grouping/import-group-setting-parser.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImportGroupIdentifierInvalidError, 3 | ImportGroupOrder, 4 | ImportGroupSettingParser, 5 | KeywordImportGroup, 6 | RegexImportGroup, 7 | } from '../../../../src/imports/import-grouping'; 8 | import { expect } from '../../setup'; 9 | 10 | describe('ImportGroupSettingParser', () => { 11 | it('should parse a simple keyword', () => { 12 | const result = ImportGroupSettingParser.parseSetting( 13 | 'Workspace', 14 | ) as KeywordImportGroup; 15 | 16 | expect(result).to.matchSnapshot(); 17 | }); 18 | 19 | it('should parse a simple regex', () => { 20 | const result = ImportGroupSettingParser.parseSetting( 21 | '/foobar/', 22 | ) as RegexImportGroup; 23 | 24 | expect(result).to.matchSnapshot(); 25 | }); 26 | 27 | it('should parse a complex keyword pattern', () => { 28 | const result = ImportGroupSettingParser.parseSetting({ 29 | identifier: 'Workspace', 30 | order: ImportGroupOrder.Desc, 31 | }) as KeywordImportGroup; 32 | 33 | expect(result).to.matchSnapshot(); 34 | }); 35 | 36 | it('should parse a complex regex pattern', () => { 37 | const result = ImportGroupSettingParser.parseSetting({ 38 | identifier: '/foobar/', 39 | order: ImportGroupOrder.Desc, 40 | }) as RegexImportGroup; 41 | 42 | expect(result).to.matchSnapshot(); 43 | }); 44 | 45 | it('should throw on non found keyword and regex', () => { 46 | const fn = () => ImportGroupSettingParser.parseSetting('whatever'); 47 | 48 | expect(fn).to.throw(ImportGroupIdentifierInvalidError); 49 | }); 50 | 51 | it('should parse a regex with "or"', () => { 52 | const result = ImportGroupSettingParser.parseSetting('/angular|react/'); 53 | 54 | expect(result).to.matchSnapshot(); 55 | }); 56 | 57 | it('should parse a regex with "@"', () => { 58 | const result = ImportGroupSettingParser.parseSetting('/@angular/'); 59 | 60 | expect(result).to.matchSnapshot(); 61 | }); 62 | 63 | it('should parse a complex regex', () => { 64 | const result = ImportGroupSettingParser.parseSetting( 65 | '/(@angular|react)/core/(.*)/', 66 | ); 67 | 68 | expect(result).to.matchSnapshot(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/imports/import-grouping/keyword-import-group.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { 3 | File, 4 | Generatable, 5 | GENERATORS, 6 | TypescriptCodeGenerator, 7 | TypescriptGenerationOptions, 8 | TypescriptParser, 9 | } from 'typescript-parser'; 10 | import { Uri, workspace } from 'vscode'; 11 | 12 | import { ImportGroupKeyword, KeywordImportGroup } from '../../../../src/imports/import-grouping'; 13 | import { ioc } from '../../../../src/ioc'; 14 | import { iocSymbols, TypescriptCodeGeneratorFactory } from '../../../../src/ioc-symbols'; 15 | import { expect } from '../../setup'; 16 | 17 | describe('KeywordImportGroup', () => { 18 | const rootPath = workspace.workspaceFolders![0].uri.fsPath; 19 | const fsFile = Uri.file( 20 | join(rootPath, 'imports', 'import-grouping', 'imports.ts'), 21 | ); 22 | let file: File; 23 | let importGroup: KeywordImportGroup; 24 | let generator: TypescriptCodeGenerator; 25 | 26 | before(() => { 27 | if (!GENERATORS[KeywordImportGroup.name]) { 28 | GENERATORS[KeywordImportGroup.name] = ( 29 | generatable: Generatable, 30 | options: TypescriptGenerationOptions, 31 | ): string => { 32 | const gen = new TypescriptCodeGenerator(options); 33 | const group = generatable as KeywordImportGroup; 34 | if (!group.imports.length) { 35 | return ''; 36 | } 37 | return ( 38 | group.sortedImports.map(imp => gen.generate(imp)).join('\n') + '\n' 39 | ); 40 | }; 41 | } 42 | }); 43 | 44 | before(async () => { 45 | const parser = ioc.get(iocSymbols.parser); 46 | generator = ioc.get( 47 | iocSymbols.generatorFactory, 48 | )(fsFile); 49 | file = await parser.parseFile(fsFile.fsPath, rootPath); 50 | }); 51 | 52 | describe(`keyword "Modules"`, () => { 53 | beforeEach(() => { 54 | importGroup = new KeywordImportGroup(ImportGroupKeyword.Modules); 55 | }); 56 | 57 | it('should process a module import', () => { 58 | expect(importGroup.processImport(file.imports[4])).to.be.true; 59 | }); 60 | 61 | it('should not process a plain import', () => { 62 | expect(importGroup.processImport(file.imports[0])).to.be.false; 63 | }); 64 | 65 | it('should not process a workspace import', () => { 66 | expect(importGroup.processImport(file.imports[2])).to.be.false; 67 | }); 68 | 69 | it('should correctly process a list of imports', () => { 70 | expect( 71 | file.imports.map(i => importGroup.processImport(i)), 72 | ).to.matchSnapshot(); 73 | }); 74 | 75 | it('should generate the correct typescript (asc)', () => { 76 | for (const imp of file.imports) { 77 | if (importGroup.processImport(imp)) { 78 | continue; 79 | } 80 | } 81 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 82 | }); 83 | 84 | it('should generate the correct typescript (desc)', () => { 85 | (importGroup as any).order = 'desc'; 86 | for (const imp of file.imports) { 87 | if (importGroup.processImport(imp)) { 88 | continue; 89 | } 90 | } 91 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 92 | }); 93 | }); 94 | 95 | describe(`keyword "Plains"`, () => { 96 | beforeEach(() => { 97 | importGroup = new KeywordImportGroup(ImportGroupKeyword.Plains); 98 | }); 99 | 100 | it('should not process a module import', () => { 101 | expect(importGroup.processImport(file.imports[4])).to.be.false; 102 | }); 103 | 104 | it('should process a plain import', () => { 105 | expect(importGroup.processImport(file.imports[0])).to.be.true; 106 | }); 107 | 108 | it('should not process a workspace import', () => { 109 | expect(importGroup.processImport(file.imports[2])).to.be.false; 110 | }); 111 | 112 | it('should correctly process a list of imports', () => { 113 | expect( 114 | file.imports.map(i => importGroup.processImport(i)), 115 | ).to.matchSnapshot(); 116 | }); 117 | 118 | it('should generate the correct typescript (asc)', () => { 119 | for (const imp of file.imports) { 120 | if (importGroup.processImport(imp)) { 121 | continue; 122 | } 123 | } 124 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 125 | }); 126 | 127 | it('should generate the correct typescript (desc)', () => { 128 | (importGroup as any).order = 'desc'; 129 | for (const imp of file.imports) { 130 | if (importGroup.processImport(imp)) { 131 | continue; 132 | } 133 | } 134 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 135 | }); 136 | }); 137 | 138 | describe(`keyword "Workspace"`, () => { 139 | beforeEach(() => { 140 | importGroup = new KeywordImportGroup(ImportGroupKeyword.Workspace); 141 | }); 142 | 143 | it('should not process a module import', () => { 144 | expect(importGroup.processImport(file.imports[4])).to.be.false; 145 | }); 146 | 147 | it('should not process a plain import', () => { 148 | expect(importGroup.processImport(file.imports[0])).to.be.false; 149 | }); 150 | 151 | it('should process a workspace import', () => { 152 | expect(importGroup.processImport(file.imports[2])).to.be.true; 153 | }); 154 | 155 | it('should correctly process a list of imports', () => { 156 | expect( 157 | file.imports.map(i => importGroup.processImport(i)), 158 | ).to.matchSnapshot(); 159 | }); 160 | 161 | it('should generate the correct typescript (asc)', () => { 162 | for (const imp of file.imports) { 163 | if (importGroup.processImport(imp)) { 164 | continue; 165 | } 166 | } 167 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 168 | }); 169 | 170 | it('should generate the correct typescript (desc)', () => { 171 | (importGroup as any).order = 'desc'; 172 | for (const imp of file.imports) { 173 | if (importGroup.processImport(imp)) { 174 | continue; 175 | } 176 | } 177 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /test/imports/import-grouping/regex-import-group.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { 3 | File, 4 | Generatable, 5 | GENERATORS, 6 | NamedImport, 7 | TypescriptCodeGenerator, 8 | TypescriptGenerationOptions, 9 | TypescriptParser, 10 | } from 'typescript-parser'; 11 | import { Uri, workspace } from 'vscode'; 12 | 13 | import { RegexImportGroup } from '../../../../src/imports/import-grouping'; 14 | import { ioc } from '../../../../src/ioc'; 15 | import { iocSymbols, TypescriptCodeGeneratorFactory } from '../../../../src/ioc-symbols'; 16 | import { expect } from '../../setup'; 17 | 18 | describe('RegexImportGroup', () => { 19 | const rootPath = workspace.workspaceFolders![0].uri.fsPath; 20 | const fsFile = Uri.file( 21 | join(rootPath, 'imports', 'import-grouping', 'imports.ts'), 22 | ); 23 | let file: File; 24 | let importGroup: RegexImportGroup; 25 | let generator: TypescriptCodeGenerator; 26 | 27 | before(() => { 28 | if (!GENERATORS[RegexImportGroup.name]) { 29 | GENERATORS[RegexImportGroup.name] = ( 30 | generatable: Generatable, 31 | options: TypescriptGenerationOptions, 32 | ): string => { 33 | const gen = new TypescriptCodeGenerator(options); 34 | const group = generatable as RegexImportGroup; 35 | if (!group.imports.length) { 36 | return ''; 37 | } 38 | return ( 39 | group.sortedImports.map(imp => gen.generate(imp)).join('\n') + '\n' 40 | ); 41 | }; 42 | } 43 | }); 44 | 45 | before(async () => { 46 | const parser = ioc.get(iocSymbols.parser); 47 | generator = ioc.get( 48 | iocSymbols.generatorFactory, 49 | )(fsFile); 50 | file = await parser.parseFile(fsFile.fsPath, rootPath); 51 | }); 52 | 53 | beforeEach(() => { 54 | importGroup = new RegexImportGroup(`/Lib/`); 55 | }); 56 | 57 | it('should process a matching import', () => { 58 | expect(importGroup.processImport(file.imports[0])).to.be.true; 59 | }); 60 | 61 | it('should not process a not matching import', () => { 62 | expect(importGroup.processImport(file.imports[2])).to.be.false; 63 | }); 64 | 65 | it('should correctly process a list of imports', () => { 66 | expect( 67 | file.imports.map(i => importGroup.processImport(i)), 68 | ).to.matchSnapshot(); 69 | }); 70 | 71 | it('should generate the correct typescript (asc)', () => { 72 | for (const imp of file.imports) { 73 | if (importGroup.processImport(imp)) { 74 | continue; 75 | } 76 | } 77 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 78 | }); 79 | 80 | it('should generate the correct typescript (desc)', () => { 81 | (importGroup as any).order = 'desc'; 82 | for (const imp of file.imports) { 83 | if (importGroup.processImport(imp)) { 84 | continue; 85 | } 86 | } 87 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 88 | }); 89 | 90 | it('should work with regex "or" conditions', () => { 91 | const group = new RegexImportGroup('/angular|react/'); 92 | const imp = new NamedImport('@angular'); 93 | const imp2 = new NamedImport('@react/core'); 94 | 95 | expect(group.processImport(imp)).to.be.true; 96 | expect(group.processImport(imp2)).to.be.true; 97 | }); 98 | 99 | it('should work with regex containing an "@"', () => { 100 | const group = new RegexImportGroup('/@angular/'); 101 | const imp = new NamedImport('@angular'); 102 | 103 | expect(group.processImport(imp)).to.be.true; 104 | }); 105 | 106 | it('should work with slash separated regex', () => { 107 | const group = new RegexImportGroup('/@angular/http/'); 108 | const imp = new NamedImport('@angular/http'); 109 | const imp2 = new NamedImport('@angular/core/component'); 110 | 111 | expect(group.processImport(imp)).to.be.true; 112 | expect(group.processImport(imp2)).to.be.false; 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/imports/import-grouping/remain-import-group.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { 3 | File, 4 | Generatable, 5 | GENERATORS, 6 | TypescriptCodeGenerator, 7 | TypescriptGenerationOptions, 8 | TypescriptParser, 9 | } from 'typescript-parser'; 10 | import { Uri, workspace } from 'vscode'; 11 | 12 | import { RemainImportGroup } from '../../../../src/imports/import-grouping'; 13 | import { ioc } from '../../../../src/ioc'; 14 | import { iocSymbols, TypescriptCodeGeneratorFactory } from '../../../../src/ioc-symbols'; 15 | import { expect } from '../../setup'; 16 | 17 | describe('RemainImportGroup', () => { 18 | const rootPath = workspace.workspaceFolders![0].uri.fsPath; 19 | const fsFile = Uri.file( 20 | join(rootPath, 'imports', 'import-grouping', 'imports.ts'), 21 | ); 22 | let file: File; 23 | let importGroup: RemainImportGroup; 24 | let generator: TypescriptCodeGenerator; 25 | 26 | before(() => { 27 | if (!GENERATORS[RemainImportGroup.name]) { 28 | GENERATORS[RemainImportGroup.name] = ( 29 | generatable: Generatable, 30 | options: TypescriptGenerationOptions, 31 | ): string => { 32 | const gen = new TypescriptCodeGenerator(options); 33 | const group = generatable as RemainImportGroup; 34 | if (!group.imports.length) { 35 | return ''; 36 | } 37 | return ( 38 | group.sortedImports.map(imp => gen.generate(imp)).join('\n') + '\n' 39 | ); 40 | }; 41 | } 42 | }); 43 | 44 | before(async () => { 45 | const parser = ioc.get(iocSymbols.parser); 46 | generator = ioc.get( 47 | iocSymbols.generatorFactory, 48 | )(fsFile); 49 | file = await parser.parseFile(fsFile.fsPath, rootPath); 50 | }); 51 | 52 | beforeEach(() => { 53 | importGroup = new RemainImportGroup(); 54 | }); 55 | 56 | it('should process all imports', () => { 57 | expect( 58 | file.imports.map(i => importGroup.processImport(i)), 59 | ).to.matchSnapshot(); 60 | }); 61 | 62 | it('should generate the correct typescript (asc)', () => { 63 | for (const imp of file.imports) { 64 | if (importGroup.processImport(imp)) { 65 | continue; 66 | } 67 | } 68 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 69 | }); 70 | 71 | it('should generate the correct typescript (desc)', () => { 72 | (importGroup as any).order = 'desc'; 73 | for (const imp of file.imports) { 74 | if (importGroup.processImport(imp)) { 75 | continue; 76 | } 77 | } 78 | expect(generator.generate(importGroup as any)).to.matchSnapshot(); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/imports/import-manager.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { ClassDeclaration, DefaultDeclaration, File, ModuleDeclaration } from 'typescript-parser'; 3 | import { Position, Range, TextDocument, Uri, window, workspace } from 'vscode'; 4 | 5 | import { ImportManager } from '../../../src/imports/import-manager'; 6 | import { ioc } from '../../../src/ioc'; 7 | import { ImportManagerProvider, iocSymbols } from '../../../src/ioc-symbols'; 8 | import { expect, getDocumentText } from '../setup'; 9 | 10 | describe('ImportManager', () => { 11 | describe('Typescript', () => { 12 | const rootPath = workspace.workspaceFolders![0].uri.fsPath; 13 | const file = Uri.file(join(rootPath, 'imports', 'import-manager-file.ts')); 14 | const provider: ImportManagerProvider = ioc.get( 15 | iocSymbols.importManager, 16 | ); 17 | let document: TextDocument; 18 | let documentText: string; 19 | 20 | before(async () => { 21 | document = await workspace.openTextDocument(file); 22 | await window.showTextDocument(document); 23 | 24 | documentText = document.getText(); 25 | }); 26 | 27 | afterEach(async () => { 28 | await window.activeTextEditor!.edit(builder => { 29 | builder.delete( 30 | new Range( 31 | new Position(0, 0), 32 | document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end, 33 | ), 34 | ); 35 | builder.insert(new Position(0, 0), documentText); 36 | }); 37 | }); 38 | 39 | describe('Constructor (via provider)', () => { 40 | it('should create a document controller', async () => { 41 | const ctrl = await provider(document); 42 | 43 | expect(ctrl).to.be.an.instanceof(ImportManager); 44 | }); 45 | 46 | it('should parse the document', async () => { 47 | const ctrl = await provider(document); 48 | 49 | expect((ctrl as any).parsedDocument).to.be.an.instanceof(File); 50 | }); 51 | 52 | it('should add an import proxy for a named import', async () => { 53 | const ctrl = await provider(document); 54 | const imps = (ctrl as any).parsedDocument.imports; 55 | 56 | expect(imps).to.matchSnapshot(); 57 | }); 58 | 59 | it('should add an import proxy for a default import', async () => { 60 | await window.activeTextEditor!.edit(builder => { 61 | builder.replace( 62 | new Range(new Position(0, 0), new Position(1, 0)), 63 | `import myDefaultExportedFunction from '../defaultExport/lateDefaultExportedElement';\n`, 64 | ); 65 | }); 66 | 67 | const ctrl = await provider(document); 68 | const imps = (ctrl as any).parsedDocument.imports; 69 | 70 | expect(imps).to.matchSnapshot(); 71 | }); 72 | 73 | it('should add multiple import proxies', async () => { 74 | await window.activeTextEditor!.edit(builder => { 75 | builder.insert( 76 | new Position(0, 0), 77 | `import myDefaultExportedFunction from '../defaultExport/lateDefaultExportedElement';\n`, 78 | ); 79 | }); 80 | 81 | const ctrl = await provider(document); 82 | const imps = (ctrl as any).parsedDocument.imports; 83 | 84 | expect(imps).to.matchSnapshot(); 85 | }); 86 | 87 | it('should not add a proxy for a namespace import', async () => { 88 | await window.activeTextEditor!.edit(builder => { 89 | builder.replace( 90 | new Range(new Position(0, 0), new Position(1, 0)), 91 | `import * as bodyParser from 'body-parser';\n`, 92 | ); 93 | }); 94 | 95 | const ctrl = await provider(document); 96 | const imps = (ctrl as any).parsedDocument.imports; 97 | 98 | expect(imps).to.matchSnapshot(); 99 | }); 100 | 101 | it('should not add a proxy for an external import', async () => { 102 | await window.activeTextEditor!.edit(builder => { 103 | builder.replace( 104 | new Range(new Position(0, 0), new Position(1, 0)), 105 | `import bodyParser = require('body-parser');\n`, 106 | ); 107 | }); 108 | 109 | const ctrl = await provider(document); 110 | const imps = (ctrl as any).parsedDocument.imports; 111 | 112 | expect(imps).to.matchSnapshot(); 113 | }); 114 | 115 | it('should not add a proxy for a string import', async () => { 116 | await window.activeTextEditor!.edit(builder => { 117 | builder.replace( 118 | new Range(new Position(0, 0), new Position(1, 0)), 119 | `import 'body-parser';\n`, 120 | ); 121 | }); 122 | 123 | const ctrl = await provider(document); 124 | const imps = (ctrl as any).parsedDocument.imports; 125 | 126 | expect(imps).to.matchSnapshot(); 127 | }); 128 | }); 129 | 130 | describe('addDeclarationImport()', () => { 131 | it('should add a normal import to the document', async () => { 132 | const ctrl = await provider(document); 133 | const declaration = new ClassDeclaration('classdeclaration', true); 134 | ctrl.addDeclarationImport({ declaration, from: '../lib' }); 135 | 136 | expect((ctrl as any).imports).to.matchSnapshot(); 137 | }); 138 | 139 | it('should add a module import to the import index', async () => { 140 | const ctrl = await provider(document); 141 | const declaration = new ModuleDeclaration('module'); 142 | ctrl.addDeclarationImport({ declaration, from: 'my-module' }); 143 | 144 | expect((ctrl as any).imports).to.matchSnapshot(); 145 | }); 146 | 147 | it('should add a default import to the import index.', async () => { 148 | const ctrl = await provider(document); 149 | 150 | const declaration = new DefaultDeclaration( 151 | 'defaultdeclaration', 152 | new File('path', rootPath, 0, 1), 153 | ); 154 | ctrl.addDeclarationImport({ declaration, from: '../lib' }); 155 | 156 | expect((ctrl as any).imports).to.matchSnapshot(); 157 | }); 158 | 159 | it('should add multiple imports to the import index', async () => { 160 | const ctrl = await provider(document); 161 | const declarations = [ 162 | new ClassDeclaration('class1', true), 163 | new ClassDeclaration('class2', true), 164 | ]; 165 | ctrl 166 | .addDeclarationImport({ 167 | declaration: declarations[0], 168 | from: '../lib1', 169 | }) 170 | .addDeclarationImport({ 171 | declaration: declarations[1], 172 | from: '../lib2', 173 | }); 174 | 175 | expect((ctrl as any).imports).to.matchSnapshot(); 176 | }); 177 | 178 | it('should add an import to an existing import index item', async () => { 179 | const ctrl = await provider(document); 180 | const declarations = [ 181 | new ClassDeclaration('class1', true), 182 | new ClassDeclaration('class2', true), 183 | ]; 184 | 185 | ctrl 186 | .addDeclarationImport({ 187 | declaration: declarations[0], 188 | from: '../lib1', 189 | }) 190 | .addDeclarationImport({ 191 | declaration: declarations[1], 192 | from: '../lib1', 193 | }); 194 | 195 | expect((ctrl as any).imports).to.matchSnapshot(); 196 | }); 197 | 198 | it('should not add the same specifier twice', async () => { 199 | const ctrl = await provider(document); 200 | const declaration = new ClassDeclaration('class', true); 201 | 202 | ctrl.addDeclarationImport({ declaration, from: '../lib' }); 203 | 204 | expect((ctrl as any).imports).to.matchSnapshot(); 205 | 206 | ctrl.addDeclarationImport({ declaration, from: '../lib' }); 207 | 208 | expect((ctrl as any).imports).to.matchSnapshot(); 209 | }); 210 | 211 | it('should add a normal import to a group', async () => { 212 | const ctrl = await provider(document); 213 | const declaration = new ClassDeclaration('class', true); 214 | 215 | expect((ctrl as any).importGroups[2]).to.matchSnapshot(); 216 | 217 | ctrl.addDeclarationImport({ declaration, from: '../lib' }); 218 | 219 | expect((ctrl as any).importGroups[2]).to.matchSnapshot(); 220 | }); 221 | 222 | it('should add an import to an existing import group', async () => { 223 | const ctrl = await provider(document); 224 | const declaration = new ClassDeclaration('class', true); 225 | const declaration2 = new ClassDeclaration('class2', true); 226 | 227 | expect((ctrl as any).importGroups[2]).to.matchSnapshot(); 228 | 229 | ctrl 230 | .addDeclarationImport({ declaration, from: '../lib1' }) 231 | .addDeclarationImport({ declaration: declaration2, from: '../lib1' }); 232 | 233 | expect((ctrl as any).importGroups[2]).to.matchSnapshot(); 234 | }); 235 | }); 236 | 237 | describe('organizeImports()', () => { 238 | it('should remove an unused import'); 239 | 240 | it('should remove an unused specifier'); 241 | 242 | it('should not remove an excluded library'); 243 | 244 | it('should merge two same libraries into one import'); 245 | }); 246 | 247 | describe('commit()', () => { 248 | it('should not touch anything if nothing changed', async () => { 249 | const ctrl = await provider(document); 250 | 251 | await window.activeTextEditor!.edit(builder => { 252 | builder.replace( 253 | document.lineAt(0).rangeIncludingLineBreak, 254 | `import {Class1} from '../resourceIndex';`, 255 | ); 256 | }); 257 | 258 | expect(await ctrl.commit()).to.be.true; 259 | expect(document.lineAt(0).text).to.equal( 260 | `import {Class1} from '../resourceIndex';`, 261 | ); 262 | }); 263 | 264 | it('should add a single new import to the document top', async () => { 265 | const ctrl = await provider(document); 266 | const declaration = new ClassDeclaration('importedclass', true); 267 | ctrl.addDeclarationImport({ declaration, from: '../imp' }); 268 | await ctrl.commit(); 269 | 270 | expect(document.lineAt(0).text).to.matchSnapshot(); 271 | }); 272 | 273 | it('should add two new imports to the document top', async () => { 274 | const ctrl = await provider(document); 275 | const declaration = new ClassDeclaration('newClass', true); 276 | const declaration2 = new ClassDeclaration('secondnewClass', true); 277 | 278 | ctrl 279 | .addDeclarationImport({ declaration, from: '../lib' }) 280 | .addDeclarationImport({ 281 | declaration: declaration2, 282 | from: '../not-same-lib', 283 | }); 284 | await ctrl.commit(); 285 | 286 | expect(getDocumentText(document, 0, 1)).to.matchSnapshot(); 287 | }); 288 | 289 | it('should add three new imports to the document top', async () => { 290 | const ctrl = await provider(document); 291 | const declaration = new ClassDeclaration('newClass', true); 292 | const declaration2 = new ClassDeclaration('secondnewClass', true); 293 | const declaration3 = new ClassDeclaration('thirdnewClass', true); 294 | 295 | ctrl 296 | .addDeclarationImport({ declaration, from: '../lib' }) 297 | .addDeclarationImport({ 298 | declaration: declaration2, 299 | from: '../not-same-lib', 300 | }) 301 | .addDeclarationImport({ 302 | declaration: declaration3, 303 | from: '../not-same-lib-again', 304 | }); 305 | await ctrl.commit(); 306 | 307 | expect(getDocumentText(document, 0, 2)).to.matchSnapshot(); 308 | }); 309 | 310 | it('should add a single new module import to the document top', async () => { 311 | const ctrl = await provider(document); 312 | const declaration = new ModuleDeclaration('newModule'); 313 | ctrl.addDeclarationImport({ declaration, from: 'new-module' }); 314 | await ctrl.commit(); 315 | 316 | expect(document.lineAt(0).text).to.matchSnapshot(); 317 | }); 318 | 319 | it('should add a single default import to the document top', async () => { 320 | const ctrl = await provider(document); 321 | const declaration = new DefaultDeclaration( 322 | 'defaultDeclaration', 323 | new File('file', rootPath, 1, 2), 324 | ); 325 | ctrl.addDeclarationImport({ declaration, from: '../lib' }); 326 | await ctrl.commit(); 327 | 328 | expect(document.lineAt(0).text).to.matchSnapshot(); 329 | }); 330 | 331 | it('should add a specifier to an existing import', async () => { 332 | const ctrl = await provider(document); 333 | const declaration = new ClassDeclaration('class2', true); 334 | ctrl.addDeclarationImport({ declaration, from: '/server/indices' }); 335 | await ctrl.commit(); 336 | 337 | expect(document.lineAt(0).text).to.matchSnapshot(); 338 | }); 339 | 340 | it('should add multiple specifier to an existing import', async () => { 341 | const ctrl = await provider(document); 342 | const declaration = new ClassDeclaration('class2', true); 343 | const declaration2 = new ClassDeclaration('class3', true); 344 | 345 | await ctrl 346 | .addDeclarationImport({ declaration, from: '/server/indices' }) 347 | .addDeclarationImport({ 348 | declaration: declaration2, 349 | from: '/server/indices', 350 | }) 351 | .commit(); 352 | 353 | expect(document.lineAt(0).text).to.matchSnapshot(); 354 | }); 355 | 356 | it('should add a specifier with a default (first) and a normal (second) import to the doc', async () => { 357 | const ctrl = await provider(document); 358 | const def = new DefaultDeclaration( 359 | 'defaultExport', 360 | new File(file.fsPath, rootPath, 1, 2), 361 | ); 362 | const dec = new ClassDeclaration('defaultClass', true); 363 | 364 | await ctrl 365 | .addDeclarationImport({ declaration: def, from: '/lib' }) 366 | .addDeclarationImport({ declaration: dec, from: '/lib' }) 367 | .commit(); 368 | 369 | expect(document.lineAt(0).text).to.matchSnapshot(); 370 | }); 371 | 372 | it('should add a specifier to an import and a new import', async () => { 373 | const ctrl = await provider(document); 374 | const declaration1 = new ClassDeclaration('class2', true); 375 | const declaration2 = new ClassDeclaration('class3', true); 376 | 377 | await ctrl 378 | .addDeclarationImport({ 379 | declaration: declaration1, 380 | from: '/server/indices', 381 | }) 382 | .addDeclarationImport({ 383 | declaration: declaration2, 384 | from: '/server/not-indices', 385 | }) 386 | .commit(); 387 | 388 | expect(getDocumentText(document, 0, 1)).to.matchSnapshot(); 389 | }); 390 | 391 | it('should convert a default import when a normal specifier is added', async () => { 392 | const ctrl = await provider(document); 393 | const def = new DefaultDeclaration( 394 | 'defaultExport', 395 | new File(file.fsPath, rootPath, 1, 2), 396 | ); 397 | const dec = new ClassDeclaration('defaultClass', true); 398 | 399 | await ctrl 400 | .addDeclarationImport({ declaration: def, from: '/server/indices' }) 401 | .commit(); 402 | 403 | expect(document.lineAt(0).text).to.matchSnapshot(); 404 | 405 | await ctrl 406 | .addDeclarationImport({ declaration: dec, from: '/server/indices' }) 407 | .commit(); 408 | 409 | expect(document.lineAt(0).text).to.matchSnapshot(); 410 | }); 411 | 412 | it('should convert a normal import when a default specifier is added', async () => { 413 | const ctrl = await provider(document); 414 | const def = new DefaultDeclaration( 415 | 'defaultExport', 416 | new File(file.fsPath, rootPath, 1, 2), 417 | ); 418 | const dec = new ClassDeclaration('defaultClass', true); 419 | 420 | await ctrl 421 | .addDeclarationImport({ declaration: dec, from: '/server/indices' }) 422 | .commit(); 423 | 424 | expect(document.lineAt(0).text).to.matchSnapshot(); 425 | 426 | await ctrl 427 | .addDeclarationImport({ declaration: def, from: '/server/indices' }) 428 | .commit(); 429 | 430 | expect(document.lineAt(0).text).to.matchSnapshot(); 431 | }); 432 | 433 | it('should render the optimized import', async () => { 434 | await window.activeTextEditor!.edit(builder => { 435 | builder.insert(new Position(5, 0), 'const foobar = new Class2();\n'); 436 | }); 437 | 438 | const ctrl = await provider(document); 439 | const declaration1 = new ClassDeclaration('Class2', true); 440 | const declaration2 = new ClassDeclaration('SomeOtherClass', true); 441 | 442 | await ctrl 443 | .addDeclarationImport({ 444 | declaration: declaration1, 445 | from: '/server/indices', 446 | }) 447 | .addDeclarationImport({ 448 | declaration: declaration2, 449 | from: '/server/not-indices', 450 | }) 451 | .commit(); 452 | 453 | expect(getDocumentText(document, 0, 1)).to.matchSnapshot(); 454 | 455 | await ctrl.organizeImports().commit(); 456 | 457 | expect(getDocumentText(document, 0, 1)).to.matchSnapshot(); 458 | }); 459 | 460 | it('should render sorted imports when optimizing', async () => { 461 | await window.activeTextEditor!.edit(builder => { 462 | builder.insert( 463 | new Position(0, 0), 464 | `import { MultiExportClass } from '../server/indices/defaultExport/multiExport';\n`, 465 | ); 466 | builder.insert( 467 | new Position(5, 0), 468 | 'const foobar = new MultiExportClass();\n', 469 | ); 470 | }); 471 | const ctrl = await provider(document); 472 | 473 | await ctrl.organizeImports().commit(); 474 | 475 | expect(getDocumentText(document, 0, 1)).to.matchSnapshot(); 476 | }); 477 | 478 | it('should render sorted specifiers when optimizing', async () => { 479 | await window.activeTextEditor!.edit(builder => { 480 | builder.insert(new Position(0, 9), 'Class2, '); 481 | builder.insert(new Position(5, 0), 'const foobar = new Class2();\n'); 482 | }); 483 | const ctrl = await provider(document); 484 | 485 | await ctrl.organizeImports().commit(); 486 | 487 | expect(document.lineAt(0).text).to.matchSnapshot(); 488 | }); 489 | }); 490 | }); 491 | }); 492 | -------------------------------------------------------------------------------- /test/imports/import-organizer.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { Position, Range, TextDocument, Uri, window, workspace } from 'vscode'; 3 | 4 | import { ImportOrganizer } from '../../../src/imports'; 5 | import { ioc } from '../../../src/ioc'; 6 | import { iocSymbols } from '../../../src/ioc-symbols'; 7 | import { expect } from '../setup'; 8 | 9 | describe('ImportOrganizer', () => { 10 | describe('import-organizer-file.ts', () => { 11 | const rootPath = workspace.workspaceFolders![0].uri.fsPath; 12 | const file = Uri.file( 13 | join(rootPath, 'imports', 'import-organizer-file.ts'), 14 | ); 15 | let document: TextDocument; 16 | let extension: any; 17 | 18 | before(async () => { 19 | document = await workspace.openTextDocument(file); 20 | await window.showTextDocument(document); 21 | 22 | extension = new ImportOrganizer( 23 | ioc.get(iocSymbols.extensionContext), 24 | ioc.get(iocSymbols.logger), 25 | ioc.get(iocSymbols.configuration), 26 | ioc.get(iocSymbols.importManager), 27 | ); 28 | }); 29 | 30 | afterEach(async () => { 31 | await window.activeTextEditor!.edit(builder => { 32 | builder.delete( 33 | new Range( 34 | new Position(0, 0), 35 | document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end, 36 | ), 37 | ); 38 | }); 39 | }); 40 | 41 | it('should not remove directly exported imports', async () => { 42 | await window.activeTextEditor!.edit(builder => { 43 | builder.insert( 44 | new Position(0, 0), 45 | `import * as Foobar from './lol'; 46 | import * as Barbaz from './foo'; 47 | 48 | export { Foobar, Barbaz } 49 | `, 50 | ); 51 | }); 52 | 53 | expect(window.activeTextEditor!.document.getText()).to.matchSnapshot(); 54 | await extension.organizeImports(); 55 | expect(window.activeTextEditor!.document.getText()).to.matchSnapshot(); 56 | }); 57 | 58 | it('should not remove directly exported default imports', async () => { 59 | await window.activeTextEditor!.edit(builder => { 60 | builder.insert( 61 | new Position(0, 0), 62 | `import Barbaz from './foo'; 63 | 64 | export { Barbaz } 65 | `, 66 | ); 67 | }); 68 | 69 | expect(window.activeTextEditor!.document.getText()).to.matchSnapshot(); 70 | await extension.organizeImports(); 71 | expect(window.activeTextEditor!.document.getText()).to.matchSnapshot(); 72 | }); 73 | 74 | it('should not remove default exported default imports', async () => { 75 | await window.activeTextEditor!.edit(builder => { 76 | builder.insert( 77 | new Position(0, 0), 78 | `import Barbaz from './foo'; 79 | 80 | export default Barbaz; 81 | `, 82 | ); 83 | }); 84 | 85 | expect(window.activeTextEditor!.document.getText()).to.matchSnapshot(); 86 | await extension.organizeImports(); 87 | expect(window.activeTextEditor!.document.getText()).to.matchSnapshot(); 88 | }); 89 | }); 90 | 91 | describe('import-organizer-file.tsx', () => { 92 | const rootPath = workspace.workspaceFolders![0].uri.fsPath; 93 | const file = Uri.file( 94 | join(rootPath, 'imports', 'import-organizer-file.tsx'), 95 | ); 96 | let document: TextDocument; 97 | let extension: any; 98 | 99 | before(async () => { 100 | document = await workspace.openTextDocument(file); 101 | await window.showTextDocument(document); 102 | 103 | extension = new ImportOrganizer( 104 | ioc.get(iocSymbols.extensionContext), 105 | ioc.get(iocSymbols.logger), 106 | ioc.get(iocSymbols.configuration), 107 | ioc.get(iocSymbols.importManager), 108 | ); 109 | }); 110 | 111 | afterEach(async () => { 112 | await window.activeTextEditor!.edit(builder => { 113 | builder.delete( 114 | new Range( 115 | new Position(0, 0), 116 | document.lineAt(document.lineCount - 1).rangeIncludingLineBreak.end, 117 | ), 118 | ); 119 | }); 120 | }); 121 | 122 | it('should not remove function that is used in tsx', async () => { 123 | await window.activeTextEditor!.edit(builder => { 124 | builder.insert( 125 | new Position(0, 0), 126 | `import { f } from './somewhere'; 127 | import * as React from 'react'; 128 | 129 | export class Comp extends React.Component { 130 | render() { 131 | return ( 132 |
{f()}
133 | ); 134 | } 135 | } 136 | `, 137 | ); 138 | }); 139 | 140 | expect(window.activeTextEditor!.document.getText()).to.matchSnapshot(); 141 | await extension.organizeImports(); 142 | expect(window.activeTextEditor!.document.getText()).to.matchSnapshot(); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 2 | // 3 | // This file is providing the test runner to use when running extension tests. 4 | // By default the test runner in use is Mocha based. 5 | // 6 | // You can provide your own test runner if you want to override it by exporting 7 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 8 | // host can call to run the tests. The test runner is expected to use console.log 9 | // to report the results back to the caller. When the tests are finished, return 10 | // a possible error to the callback or null if none. 11 | import { join, relative } from 'path'; 12 | import { ExtensionContext, Memento } from 'vscode'; 13 | 14 | declare var global: any; 15 | 16 | class ContextMock implements ExtensionContext { 17 | public subscriptions: { dispose(): any }[] = []; 18 | public workspaceState: Memento = undefined as any; 19 | public globalState: Memento = undefined as any; 20 | public extensionPath: string = ''; 21 | public storagePath: string = ''; 22 | public asAbsolutePath(path: string): string { 23 | return relative(global['rootPath'], path); 24 | } 25 | } 26 | 27 | // Prepare for snapshot (sigh) tests. 28 | // HACK 29 | global['rootPath'] = join(__dirname, '..', '..'); 30 | // END HACK 31 | 32 | const testRunner = require('vscode/lib/testrunner'); 33 | 34 | // You can directly control Mocha options by uncommenting the following lines 35 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 36 | const options: any = { 37 | ui: 'bdd', 38 | useColors: true, 39 | timeout: 5000, 40 | }; 41 | 42 | if (process.env.EXT_DEBUG) { 43 | options.timeout = 2 * 60 * 60 * 1000; 44 | } 45 | 46 | testRunner.configure(options); 47 | 48 | const { default: ioc } = require('../src/ioc'); 49 | const { default: iocSymbols } = require('../src/ioc-symbols'); 50 | ioc.bind(iocSymbols.extensionContext).toConstantValue(new ContextMock()); 51 | 52 | module.exports = testRunner; 53 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import { expect as chaiExpect, use } from 'chai'; 2 | import { EOL } from 'os'; 3 | import { join, parse } from 'path'; 4 | import sinonChai = require('sinon-chai'); 5 | import { DeclarationIndex } from 'typescript-parser'; 6 | import { TextDocument } from 'vscode'; 7 | 8 | const chaiJestSnapshot = require('chai-jest-snapshot'); 9 | 10 | declare global { 11 | namespace Chai { 12 | interface Assertion { 13 | matchSnapshot(): Assertion; 14 | } 15 | } 16 | } 17 | 18 | use(chaiJestSnapshot); 19 | use(sinonChai); 20 | 21 | before(() => { 22 | chaiJestSnapshot.resetSnapshotRegistry(); 23 | }); 24 | 25 | beforeEach(function (): void { 26 | const fileFromTestRoot = (this.currentTest as any).file 27 | .replace(/.*out[\\/]/, '') 28 | .replace(/\.js$/, '.ts'); 29 | const tsFile = parse(join((global as any)['rootPath'], fileFromTestRoot)); 30 | const snapPath = join(tsFile.dir, '__snapshots__', tsFile.base); 31 | chaiJestSnapshot.configureUsingMochaContext(this); 32 | chaiJestSnapshot.setFilename(`${snapPath}.snap`); 33 | }); 34 | 35 | export const expect = chaiExpect; 36 | 37 | export function wait(ms: number): Promise { 38 | return new Promise(resolve => setTimeout(() => resolve(), ms)); 39 | } 40 | 41 | export function getDocumentText( 42 | document: TextDocument, 43 | lineFrom: number, 44 | lineTo: number, 45 | ): string { 46 | const lines: string[] = []; 47 | for (let line = lineFrom; line <= lineTo; line++) { 48 | lines.push(document.lineAt(line).text); 49 | } 50 | return lines.join(EOL) + EOL; 51 | } 52 | 53 | export function waitForIndexReady(index: DeclarationIndex): Promise { 54 | return new Promise((resolve, _reject) => { 55 | if (index.indexReady) { 56 | resolve(); 57 | } 58 | 59 | const interval = setInterval(() => { 60 | if (index.indexReady) { 61 | clearInterval(interval); 62 | resolve(); 63 | } 64 | }, 50); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/utilities/__snapshots__/utility-functions.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`utility functions getImportInsertPosition() should return correct position for commented file 1`] = ` 4 | Object { 5 | "character": 0, 6 | "line": 1, 7 | } 8 | `; 9 | 10 | exports[`utility functions getImportInsertPosition() should return correct position for js block comment open 1`] = ` 11 | Object { 12 | "character": 0, 13 | "line": 1, 14 | } 15 | `; 16 | 17 | exports[`utility functions getImportInsertPosition() should return correct position for jsdoc comment close 1`] = ` 18 | Object { 19 | "character": 0, 20 | "line": 1, 21 | } 22 | `; 23 | 24 | exports[`utility functions getImportInsertPosition() should return correct position for jsdoc comment line 1`] = ` 25 | Object { 26 | "character": 0, 27 | "line": 1, 28 | } 29 | `; 30 | 31 | exports[`utility functions getImportInsertPosition() should return correct position for jsdoc comment open 1`] = ` 32 | Object { 33 | "character": 0, 34 | "line": 1, 35 | } 36 | `; 37 | 38 | exports[`utility functions getImportInsertPosition() should return correct position for shebang (#!) 1`] = ` 39 | Object { 40 | "character": 0, 41 | "line": 1, 42 | } 43 | `; 44 | 45 | exports[`utility functions getImportInsertPosition() should return correct position for use strict 1`] = ` 46 | Object { 47 | "character": 0, 48 | "line": 1, 49 | } 50 | `; 51 | 52 | exports[`utility functions getImportInsertPosition() should return the top position if empty file 1`] = ` 53 | Object { 54 | "character": 0, 55 | "line": 0, 56 | } 57 | `; 58 | 59 | exports[`utility functions getImportInsertPosition() should return top position if no editor is specified 1`] = ` 60 | Object { 61 | "character": 0, 62 | "line": 0, 63 | } 64 | `; 65 | 66 | exports[`utility functions importSortByFirstSpecifier should sort according to first specifier/alias, falling back to module path 1`] = ` 67 | Array [ 68 | "./anotherFile", 69 | "coolEffectLib", 70 | "./myFile", 71 | "myLib", 72 | "anotherLib", 73 | "./workspaceSideEffectLib", 74 | ] 75 | `; 76 | -------------------------------------------------------------------------------- /test/utilities/utility-functions.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { TypescriptParser } from 'typescript-parser'; 3 | import { workspace } from 'vscode'; 4 | 5 | import { ImportGroupKeyword, KeywordImportGroup, RegexImportGroup } from '../../src/imports/import-grouping'; 6 | import { ioc } from '../../src/ioc'; 7 | import { iocSymbols } from '../../src/ioc-symbols'; 8 | import { 9 | getImportInsertPosition, 10 | importGroupSortForPrecedence, 11 | importSortByFirstSpecifier, 12 | } from '../../src/utilities/utility-functions'; 13 | import { expect } from '../setup'; 14 | 15 | describe('utility functions', () => { 16 | describe('getImportInsertPosition()', () => { 17 | class MockDocument { 18 | constructor(private documentText: string) {} 19 | 20 | public getText(): string { 21 | return this.documentText; 22 | } 23 | } 24 | 25 | it('should return top position if no editor is specified', () => { 26 | const pos = getImportInsertPosition(undefined); 27 | expect(pos).to.matchSnapshot(); 28 | }); 29 | 30 | it('should return the top position if empty file', () => { 31 | const pos = getImportInsertPosition({ 32 | document: new MockDocument(''), 33 | } as any); 34 | expect(pos).to.matchSnapshot(); 35 | }); 36 | 37 | it('should return correct position for commented file', () => { 38 | const pos = getImportInsertPosition({ 39 | document: new MockDocument( 40 | ' // This is a file header\nStart of file\n', 41 | ), 42 | } as any); 43 | expect(pos).to.matchSnapshot(); 44 | }); 45 | 46 | it('should return correct position for use strict', () => { 47 | const pos = getImportInsertPosition({ 48 | document: new MockDocument(`'use strict'\nStart of file\n`), 49 | } as any); 50 | expect(pos).to.matchSnapshot(); 51 | }); 52 | 53 | it('should return correct position for jsdoc comment open', () => { 54 | const pos = getImportInsertPosition({ 55 | document: new MockDocument('/** start of a jsdoc\n'), 56 | } as any); 57 | expect(pos).to.matchSnapshot(); 58 | }); 59 | 60 | it('should return correct position for js block comment open', () => { 61 | const pos = getImportInsertPosition({ 62 | document: new MockDocument('/* yay\n'), 63 | } as any); 64 | expect(pos).to.matchSnapshot(); 65 | }); 66 | 67 | it('should return correct position for jsdoc comment line', () => { 68 | const pos = getImportInsertPosition({ 69 | document: new MockDocument(' * jsdoc line\n'), 70 | } as any); 71 | expect(pos).to.matchSnapshot(); 72 | }); 73 | 74 | it('should return correct position for jsdoc comment close', () => { 75 | const pos = getImportInsertPosition({ 76 | document: new MockDocument('*/\n'), 77 | } as any); 78 | expect(pos).to.matchSnapshot(); 79 | }); 80 | 81 | it('should return correct position for shebang (#!)', () => { 82 | const pos = getImportInsertPosition({ 83 | document: new MockDocument('#!\n'), 84 | } as any); 85 | expect(pos).to.matchSnapshot(); 86 | }); 87 | }); 88 | 89 | describe('importGroupSortForPrecedence', () => { 90 | it('should prioritize regexes, leaving original order untouched besides that', () => { 91 | const initialList = [ 92 | new KeywordImportGroup(ImportGroupKeyword.Modules), 93 | new KeywordImportGroup(ImportGroupKeyword.Plains), 94 | new RegexImportGroup('/cool-library/'), 95 | new RegexImportGroup('/cooler-library/'), 96 | new KeywordImportGroup(ImportGroupKeyword.Workspace), 97 | ]; 98 | const expectedList = initialList 99 | .slice(2, 4) 100 | .concat(initialList.slice(0, 2)) 101 | .concat(initialList.slice(4)); 102 | 103 | expect(importGroupSortForPrecedence(initialList)).to.deep.equal( 104 | expectedList, 105 | 'Regex Import Groups should appear first (and that should be the only change)', 106 | ); 107 | }); 108 | }); 109 | 110 | describe('importSortByFirstSpecifier', () => { 111 | const parser = ioc.get(iocSymbols.parser); 112 | const rootPath = workspace.workspaceFolders![0].uri.fsPath; 113 | 114 | it('should sort according to first specifier/alias, falling back to module path', async () => { 115 | const file = await parser.parseFile( 116 | join(rootPath, 'utilities', 'imports-for-specifier-sort.ts'), 117 | rootPath, 118 | ); 119 | 120 | const result = [...file.imports].sort(importSortByFirstSpecifier); 121 | expect(result.map(i => i.libraryName)).to.matchSnapshot(); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./config/tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "watch": true 6 | }, 7 | "include": ["./src/**/*", "./test/**/*"], 8 | "exclude": [] 9 | } 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@smartive/tslint-config", 3 | "rules": { 4 | "import-name": false, 5 | "ter-arrow-parens": false 6 | } 7 | } 8 | --------------------------------------------------------------------------------