From 219389f28fc3b36a38ecf521044bc50206cf68e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BChler?= Date: Thu, 26 Oct 2017 07:54:16 +0200 Subject: [PATCH] feat: add multi root workspace support (#325) closes #283. --- .travis.yml | 10 +- .vscode/launch.json | 35 +- config/tsconfig.base.json | 6 +- package.json | 61 +-- src/common/config/CodeCompletionConfig.ts | 28 ++ src/common/config/CodeOutlineConfig.ts | 16 +- src/common/config/ExtensionConfig.ts | 39 +- src/common/config/ResolverConfig.ts | 11 + src/common/config/index.ts | 1 + src/common/factories/index.ts | 5 + src/common/helpers/DeclarationIndexHelpers.ts | 95 ++++- src/extension/IoC.ts | 36 +- src/extension/IoCSymbols.ts | 3 +- .../MissingImplementationInClassCreator.ts | 39 +- .../code-actions/MissingImportCreator.ts | 28 +- .../config/VscodeCodeCompletionConfig.ts | 30 ++ .../config/VscodeCodeOutlineConfig.ts | 30 ++ src/extension/config/VscodeExtensionConfig.ts | 86 +++++ .../VscodeResolverConfig.ts} | 149 ++------ .../extensions/CodeActionExtension.ts | 23 +- .../extensions/CodeCompletionExtension.ts | 76 ++-- .../DocumentSymbolStructureExtension.ts | 16 +- .../extensions/ImportResolveExtension.ts | 346 ++++-------------- .../OrganizeImportsOnSaveExtension.ts | 7 +- src/extension/managers/ClassManager.ts | 53 ++- src/extension/managers/ImportManager.ts | 36 +- .../utilities/DeclarationIndexMapper.ts | 174 +++++++++ src/extension/utilities/VscodeLogger.ts | 26 +- test/_workspace/.vscode/settings.json | 4 +- test/_workspace_2/file1.ts | 0 test/_workspace_2/file2.ts | 3 + .../MultiRootIndices.test.ts | 6 + .../{ => multi-root-workspace-tests}/index.ts | 5 +- test/multi-root.code-workspace | 20 + .../common/helpers/ImportHelpers.test.ts | 2 +- .../extensions/CodeActionExtension.test.ts | 55 +-- .../CodeCompletionExtension.test.ts | 47 ++- .../DocumentSymbolStructureExtension.test.ts | 27 +- .../extensions/ImportResolveExtension.test.ts | 51 ++- .../OrganizeImportsOnSaveExtension.test.ts | 10 +- .../ImportGroupSettingParser.test.ts | 5 +- .../KeywordImportGroup.test.ts | 14 +- .../import-grouping/RegexImportGroup.test.ts | 15 +- .../import-grouping/RemainImportGroup.test.ts | 15 +- .../extension/managers/ClassManager.test.ts | 19 +- .../extension/managers/ImportManager.test.ts | 26 +- test/single-workspace-tests/index.ts | 44 +++ 47 files changed, 1077 insertions(+), 756 deletions(-) create mode 100644 src/common/config/CodeCompletionConfig.ts create mode 100644 src/extension/config/VscodeCodeCompletionConfig.ts create mode 100644 src/extension/config/VscodeCodeOutlineConfig.ts create mode 100644 src/extension/config/VscodeExtensionConfig.ts rename src/extension/{VscodeExtensionConfig.ts => config/VscodeResolverConfig.ts} (60%) create mode 100644 src/extension/utilities/DeclarationIndexMapper.ts create mode 100644 test/_workspace_2/file1.ts create mode 100644 test/_workspace_2/file2.ts create mode 100644 test/multi-root-workspace-tests/MultiRootIndices.test.ts rename test/{ => multi-root-workspace-tests}/index.ts (92%) create mode 100644 test/multi-root.code-workspace rename test/{ => single-workspace-tests}/common/helpers/ImportHelpers.test.ts (96%) rename test/{ => single-workspace-tests}/extension/extensions/CodeActionExtension.test.ts (90%) rename test/{ => single-workspace-tests}/extension/extensions/CodeCompletionExtension.test.ts (72%) rename test/{ => single-workspace-tests}/extension/extensions/DocumentSymbolStructureExtension.test.ts (74%) rename test/{ => single-workspace-tests}/extension/extensions/ImportResolveExtension.test.ts (90%) rename test/{ => single-workspace-tests}/extension/extensions/OrganizeImportsOnSaveExtension.test.ts (91%) rename test/{ => single-workspace-tests}/extension/import-grouping/ImportGroupSettingParser.test.ts (97%) rename test/{ => single-workspace-tests}/extension/import-grouping/KeywordImportGroup.test.ts (92%) rename test/{ => single-workspace-tests}/extension/import-grouping/RegexImportGroup.test.ts (86%) rename test/{ => single-workspace-tests}/extension/import-grouping/RemainImportGroup.test.ts (81%) rename test/{ => single-workspace-tests}/extension/managers/ClassManager.test.ts (96%) rename test/{ => single-workspace-tests}/extension/managers/ImportManager.test.ts (97%) create mode 100644 test/single-workspace-tests/index.ts diff --git a/.travis.yml b/.travis.yml index 435d94f..8d5f6f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,6 @@ node_js: notifications: email: false -env: - global: - - CODE_TESTS_WORKSPACE=$TRAVIS_BUILD_DIR/test/_workspace - os: - linux @@ -32,8 +28,8 @@ addons: before_install: - if [ $TRAVIS_OS_NAME == "linux" ]; then export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; - sh -e /etc/init.d/xvfb start; - sleep 3; + sh -e /etc/init.d/xvfb start; + sleep 3; fi - rvm get stable - rvm install 2.4 @@ -42,7 +38,7 @@ before_install: install: - yarn install - + before_script: - greenkeeper-lockfile-update - yarn run build diff --git a/.vscode/launch.json b/.vscode/launch.json index 98daa37..09c7f69 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,14 +17,45 @@ ] }, { - "name": "Launch Tests", + "name": "Launch Extension Multi-Root", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceRoot}/test/multi-root.code-workspace", + "--extensionDevelopmentPath=${workspaceRoot}" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/out/src/**/*.js" + ] + }, + { + "name": "Launch Tests (single Workspace)", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ "${workspaceRoot}/test/_workspace", "--extensionDevelopmentPath=${workspaceRoot}", - "--extensionTestsPath=${workspaceRoot}/out/test" + "--extensionTestsPath=${workspaceRoot}/out/test/single-workspace-tests/" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/out/test/single-workspace-tests/**/*.js" + ] + }, + { + "name": "Launch Tests (multi-root Workspace)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceRoot}/test/multi-root.code-workspace", + "--extensionDevelopmentPath=${workspaceRoot}", + "--extensionTestsPath=${workspaceRoot}/out/test/multi-root-workspace-tests/" ], "stopOnEntry": false, "sourceMaps": true, diff --git a/config/tsconfig.base.json b/config/tsconfig.base.json index efe393c..3287faf 100644 --- a/config/tsconfig.base.json +++ b/config/tsconfig.base.json @@ -4,7 +4,8 @@ "target": "es6", "outDir": "../out", "lib": [ - "es6" + "es6", + "es2017" ], "rootDir": "..", "emitDecoratorMetadata": true, @@ -20,6 +21,7 @@ "../test/**/*" ], "exclude": [ - "../test/_workspace" + "../test/_workspace", + "../test/_workspace_2" ] } diff --git a/package.json b/package.json index 7df45c6..c87492b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "version": "0.0.0-development", "publisher": "rbbit", "engines": { - "vscode": "^1.13.0", + "vscode": "^1.17.0", "node": ">=8.0.0", "npm": ">=5.3.0" }, @@ -80,13 +80,15 @@ "predevelop": "del-cli ./out", "develop": "tsc", "postinstall": "test -f ./node_modules/vscode/bin/install && node ./node_modules/vscode/bin/install || echo 'vscode/bin/install not found'", - "pretest": "del-cli ./out && tsc -p ./config/tsconfig.test.json && yarn run lint", - "test": "node ./node_modules/vscode/bin/test", "lint": "tslint -c tslint.json --project ./config/tsconfig.build.json", + "pretest": "del-cli ./out && tsc -p ./config/tsconfig.test.json && yarn lint", + "test": "yarn test:single-workspace", + "test:single-workspace": "CODE_TESTS_WORKSPACE=$(pwd)/test/_workspace CODE_TESTS_PATH=$(pwd)/out/test/single-workspace-tests node ./node_modules/vscode/bin/test", + "test:multi-workspace": "CODE_TESTS_WORKSPACE=$(pwd)/test/multi-root.code-workspace CODE_TESTS_PATH=$(pwd)/out/test/multi-root-workspace-tests node ./node_modules/vscode/bin/test", "build": "del-cli ./out && tsc -p ./config/tsconfig.build.json", - "package": "yarn run build && del-cli './*.vsix' && vsce package", + "package": "yarn build && del-cli './*.vsix' && vsce package", "semantic-release-pre": "semantic-release pre", - "semantic-release": "yarn run semantic-release-pre && vsce package && vsce publish -p $VSCE_TOKEN && yarn install && semantic-release post" + "semantic-release": "yarn semantic-release-pre && vsce package && vsce publish -p $VSCE_TOKEN && yarn install && semantic-release post" }, "devDependencies": { "@types/chai": "^4.0.4", @@ -199,25 +201,29 @@ "All" ], "default": "Warnings", - "description": "Defines the log output level in the output window." + "description": "Defines the log output level in the output window.", + "scope": "window" }, - "typescriptHero.completionSortMode": { + "typescriptHero.codeCompletion.completionSortMode": { "enum": [ "default", "bottom" ], "default": "default", - "description": "Import completion sort order" + "description": "Defines the sortmode of the code completion in intellisense (bottom means sorted to bottom).", + "scope": "resource" }, "typescriptHero.resolver.insertSpaceBeforeAndAfterImportBraces": { "type": "boolean", "default": true, - "description": "Defines if there should be a space inside the curly braces of an import statement." + "description": "Defines if there should be a space inside the curly braces of an import statement.", + "scope": "resource" }, "typescriptHero.resolver.insertSemicolons": { "type": "boolean", "default": true, - "description": "Defines if there should be a semicolon at the end of a statement." + "description": "Defines if there should be a semicolon at the end of a statement.", + "scope": "resource" }, "typescriptHero.resolver.stringQuoteStyle": { "enum": [ @@ -225,7 +231,8 @@ "\"" ], "default": "'", - "description": "Defines if single or double quotes should be used." + "description": "Defines if single or double quotes should be used.", + "scope": "resource" }, "typescriptHero.resolver.ignorePatterns": { "type": "array", @@ -238,24 +245,28 @@ "out", "dist" ], - "description": "Defines partial pathes that are ignored during indexing (e.g. 'node_modules' would exclude all modules)." + "description": "Defines partial pathes that are ignored during indexing (e.g. 'node_modules' would exclude all modules).", + "scope": "resource" }, "typescriptHero.resolver.multiLineWrapThreshold": { "type": "number", "minimum": 1, "multipleOf": 1, "default": 125, - "description": "Defines the threshold when an import should be wrapped into a multiline import." + "description": "Defines the threshold when an import should be wrapped into a multiline import.", + "scope": "resource" }, "typescriptHero.resolver.multiLineTrailingComma": { "type": "boolean", "default": true, - "description": "Defined if multi line imports contain the last trailing comma." + "description": "Defined if multi line imports contain the last trailing comma.", + "scope": "resource" }, "typescriptHero.resolver.disableImportsSorting": { "type": "boolean", "default": false, - "description": "Defines if sorting is disable during organize imports." + "description": "Defines if sorting is disable during organize imports.", + "scope": "resource" }, "typescriptHero.resolver.disableImportRemovalOnOrganize": { "type": "boolean", @@ -265,7 +276,8 @@ "typescriptHero.resolver.organizeOnSave": { "type": "boolean", "default": false, - "description": "Defines if the imports should be organized on save." + "description": "Defines if the imports should be organized on save.", + "scope": "resource" }, "typescriptHero.resolver.ignoreImportsForOrganize": { "type": "array", @@ -276,7 +288,8 @@ "default": [ "react" ], - "description": "Defines imports (libraries, so the 'from' part), which are not removed during 'organize imports'." + "description": "Defines imports (libraries, so the 'from' part), which are not removed during 'organize imports'.", + "scope": "resource" }, "typescriptHero.resolver.importGroups": { "type": "array", @@ -345,7 +358,8 @@ "Modules", "Workspace" ], - "description": "Defines the groups of the imports ordering. Multiple groups possible, see readme for instructions." + "description": "Defines the groups of the imports ordering. Multiple groups possible, see readme for instructions.", + "scope": "resource" }, "typescriptHero.resolver.resolverMode": { "enum": [ @@ -353,18 +367,21 @@ "ES6", "Both" ], - "default": "TypeScript", - "description": "Defines the mode of the symbol resolver (i.e. if JavaScript files should be indexed as well (experimental)." + "default": "Both", + "description": "Defines the mode of the symbol resolver. (Note that JavaScript mode only indexes workspace files)", + "scope": "resource" }, "typescriptHero.resolver.promptForSpecifiers": { "type": "boolean", "default": true, - "description": "Defines if typescript hero should ask the user for default specifiers or duplicate specifier aliases." + "description": "Defines if typescript hero should ask the user for default specifiers or duplicate specifier aliases.", + "scope": "window" }, "typescriptHero.codeOutline.enabled": { "type": "boolean", "default": true, - "description": "Defines if the code outline feature (and window) are enabled or not." + "description": "Defines if the code outline feature (and window) are enabled or not.", + "scope": "window" } } } diff --git a/src/common/config/CodeCompletionConfig.ts b/src/common/config/CodeCompletionConfig.ts new file mode 100644 index 0000000..617bea6 --- /dev/null +++ b/src/common/config/CodeCompletionConfig.ts @@ -0,0 +1,28 @@ +import { Uri } from 'vscode'; + +/** + * Configuration interface for the code outline feature. + * + * @export + * @interface CodeCompletionConfig + */ +export interface CodeCompletionConfig { + /** + * The given resource URI (if any) for the actual configuration. + * Is needed to determine the actual config values for multi root environments. + * + * @readonly + * @type {Uri} + * @memberof CodeCompletionConfig + */ + resource?: Uri; + + /** + * Defines the used completion sort mode (i.e. if the completions should be sorted to the bottom of the list). + * + * @readonly + * @type {('default' | 'bottom')} + * @memberof CodeCompletionConfig + */ + completionSortMode: 'default' | 'bottom'; +} diff --git a/src/common/config/CodeOutlineConfig.ts b/src/common/config/CodeOutlineConfig.ts index 0ae656f..e6c5aee 100644 --- a/src/common/config/CodeOutlineConfig.ts +++ b/src/common/config/CodeOutlineConfig.ts @@ -1,13 +1,25 @@ +import { Uri } from 'vscode'; + /** * Configuration interface for the code outline feature. - * + * * @export * @interface CodeOutlineConfig */ export interface CodeOutlineConfig { + /** + * The given resource URI (if any) for the actual configuration. + * Is needed to determine the actual config values for multi root environments. + * + * @readonly + * @type {Uri} + * @memberof CodeOutlineConfig + */ + resource?: Uri; + /** * Defined if the code outline feature is enabled or not. - * + * * @type {boolean} * @memberof CodeOutlineConfig */ diff --git a/src/common/config/ExtensionConfig.ts b/src/common/config/ExtensionConfig.ts index a090178..7111110 100644 --- a/src/common/config/ExtensionConfig.ts +++ b/src/common/config/ExtensionConfig.ts @@ -1,26 +1,47 @@ +import { Uri } from 'vscode'; + +import { CodeCompletionConfig } from './CodeCompletionConfig'; import { CodeOutlineConfig } from './CodeOutlineConfig'; import { ResolverConfig } from './ResolverConfig'; /** * Configuration interface for TypeScript Hero * Contains all exposed config endpoints. - * + * * @export * @interface ExtensionConfig */ export interface ExtensionConfig { + /** + * The given resource URI (if any) for the actual configuration. + * Is needed to determine the actual config values for multi root environments. + * + * @readonly + * @type {Uri} + * @memberof ExtensionConfig + */ + resource?: Uri; + /** * The actual log level. - * + * * @readonly * @type {string} * @memberof ExtensionConfig */ verbosity: string; + /** + * Returns a list of possible language IDs that are registered within this extension. + * + * @type {string[]} + * @memberof ExtensionConfig + */ + possibleLanguages: string[]; + /** * Configuration object for the resolver extension. - * + * * @readonly * @type {ResolverConfig} * @memberof ExtensionConfig @@ -29,7 +50,7 @@ export interface ExtensionConfig { /** * Configuration object for the code outline extension. - * + * * @readonly * @type {CodeOutlineConfig} * @memberof ExtensionConfig @@ -37,13 +58,11 @@ export interface ExtensionConfig { codeOutline: CodeOutlineConfig; /** - * Completion sorting mode: - * default: Use default VSCode sorting mode - * bottom: Push to bottom - * + * Configuration object for the code completion extension. + * * @readonly - * @type {'default'|'bottom'} + * @type {CodeCompletionConfig} * @memberof ExtensionConfig */ - completionSortMode: 'default' | 'bottom'; + codeCompletion: CodeCompletionConfig; } diff --git a/src/common/config/ResolverConfig.ts b/src/common/config/ResolverConfig.ts index 85ff6d1..0457f80 100644 --- a/src/common/config/ResolverConfig.ts +++ b/src/common/config/ResolverConfig.ts @@ -1,4 +1,5 @@ import { TypescriptGenerationOptions } from 'typescript-parser'; +import { Uri } from 'vscode'; import { ImportGroup } from '../../extension/import-grouping'; import { ResolverMode } from '../enums'; @@ -9,6 +10,16 @@ import { ResolverMode } from '../enums'; * @interface ResolverConfig */ export interface ResolverConfig { + /** + * The given resource URI (if any) for the actual configuration. + * Is needed to determine the actual config values for multi root environments. + * + * @readonly + * @type {Uri} + * @memberof ResolverConfig + */ + resource?: Uri; + /** * Defines, if there should be a space between the brace and the import specifiers. * {Symbol} vs { Symbol } diff --git a/src/common/config/index.ts b/src/common/config/index.ts index 4a95f94..04b8fa7 100644 --- a/src/common/config/index.ts +++ b/src/common/config/index.ts @@ -1,3 +1,4 @@ export * from './ExtensionConfig'; export * from './ResolverConfig'; export * from './CodeOutlineConfig'; +export * from './CodeCompletionConfig'; diff --git a/src/common/factories/index.ts b/src/common/factories/index.ts index 53b7469..3b2f7ee 100644 --- a/src/common/factories/index.ts +++ b/src/common/factories/index.ts @@ -1,6 +1,11 @@ import { TypescriptCodeGenerator } from 'typescript-parser'; +import { Uri } from 'vscode'; + +import { ExtensionConfig } from '../config'; /** * IOC Factory for the {TypescriptCodeGenerator}. */ export type TypescriptCodeGeneratorFactory = () => TypescriptCodeGenerator; + +export type ConfigFactory = (resource?: Uri) => ExtensionConfig; diff --git a/src/common/helpers/DeclarationIndexHelpers.ts b/src/common/helpers/DeclarationIndexHelpers.ts index f131565..fd27adc 100644 --- a/src/common/helpers/DeclarationIndexHelpers.ts +++ b/src/common/helpers/DeclarationIndexHelpers.ts @@ -1,5 +1,9 @@ +import { existsSync } from 'fs'; import { join, normalize, parse, relative } from 'path'; import { DeclarationInfo, ExternalModuleImport, Import, NamedImport, NamespaceImport } from 'typescript-parser'; +import { RelativePattern, Uri, workspace, WorkspaceFolder } from 'vscode'; + +import { ExtensionConfig } from '../config'; /** * Calculates a list of declarationInfos filtered by the already imported ones in the given document. @@ -24,15 +28,11 @@ export function getDeclarationsFilteredByImports( const importedLib = getAbsolutLibraryName(tsImport.libraryName, documentPath, rootPath); if (tsImport instanceof NamedImport) { - declarations = declarations - .filter(o => o.from !== importedLib || !(tsImport as NamedImport).specifiers - .some(s => s.specifier === o.declaration.name)); - // if (tsImport.defaultAlias) { - // else if (tsImport instanceof DefaultImport) { - // declarations = declarations - // .filter(o => (!(o.declaration instanceof DefaultDeclaration) || importedLib !== o.from)); - // } - // } + declarations = declarations.filter( + o => o.from !== importedLib || + !tsImport.specifiers.some(s => s.specifier === o.declaration.name), + // || tsImport.defaultAlias !== o.declaration.name, + ); } else if (tsImport instanceof NamespaceImport || tsImport instanceof ExternalModuleImport) { declarations = declarations.filter(o => o.from !== tsImport.libraryName); } @@ -88,3 +88,80 @@ export function getRelativeLibraryName(library: string, actualFilePath: string, } return relativePath.replace(/\\/g, '/'); } + +/** + * This function searches for files in a specific workspace folder. The files are relative to the given + * workspace folder and the searched type is determined by the configuration of the extension (TS, JS or Both mode). + * + * @export + * @param {ExtensionConfig} config + * @param {WorkspaceFolder} workspaceFolder + * @returns {Promise} + */ +export async function findFiles(config: ExtensionConfig, workspaceFolder: WorkspaceFolder): Promise { + const searches: PromiseLike[] = [ + workspace.findFiles( + new RelativePattern(workspaceFolder, `{${config.resolver.resolverModeFileGlobs.join(',')}}`), + new RelativePattern(workspaceFolder, '{**/node_modules/**,**/typings/**}'), + ), + ]; + + // TODO: check the package json and index javascript file in node_modules (?) + + let globs: string[] = []; + let ignores = ['**/typings/**']; + const excludePatterns = config.resolver.ignorePatterns; + const rootPath = workspaceFolder.uri.fsPath; + + if (rootPath && existsSync(join(rootPath, 'package.json'))) { + const packageJson = require(join(rootPath, 'package.json')); + if (packageJson['dependencies']) { + globs = globs.concat( + Object.keys(packageJson['dependencies']).filter(o => excludePatterns.indexOf(o) < 0) + .map(o => `**/node_modules/${o}/**/*.d.ts`), + ); + ignores = ignores.concat( + Object.keys(packageJson['dependencies']).filter(o => excludePatterns.indexOf(o) < 0) + .map(o => `**/node_modules/${o}/node_modules/**`), + ); + } + if (packageJson['devDependencies']) { + globs = globs.concat( + Object.keys(packageJson['devDependencies']).filter(o => excludePatterns.indexOf(o) < 0) + .map(o => `**/node_modules/${o}/**/*.d.ts`), + ); + ignores = ignores.concat( + Object.keys(packageJson['devDependencies']).filter(o => excludePatterns.indexOf(o) < 0) + .map(o => `**/node_modules/${o}/node_modules/**`), + ); + } + } else { + globs.push('**/node_modules/**/*.d.ts'); + } + + searches.push( + workspace.findFiles( + new RelativePattern(workspaceFolder, `{${globs.join(',')}}`), + new RelativePattern(workspaceFolder, `{${ignores.join(',')}}`), + ), + ); + + searches.push( + workspace.findFiles( + new RelativePattern(workspaceFolder, '**/typings/**/*.d.ts'), + new RelativePattern(workspaceFolder, '**/node_modules/**'), + ), + ); + + let uris = await Promise.all(searches); + + uris = uris.map((o, idx) => idx === 0 ? + o.filter( + f => f.fsPath + .replace(rootPath || '', '') + .split(/\\|\//) + .every(p => excludePatterns.indexOf(p) < 0)) : + o, + ); + return uris.reduce((all, cur) => all.concat(cur), []).map(o => o.fsPath); +} diff --git a/src/extension/IoC.ts b/src/extension/IoC.ts index 095da96..fd4545f 100644 --- a/src/extension/IoC.ts +++ b/src/extension/IoC.ts @@ -1,11 +1,13 @@ import { Container as IoCContainer, interfaces } from 'inversify'; import inversifyInjectDecorators from 'inversify-inject-decorators'; -import { DeclarationIndex, TypescriptCodeGenerator, TypescriptParser } from 'typescript-parser'; -import { ExtensionContext, workspace } from 'vscode'; +import { TypescriptCodeGenerator, TypescriptParser } from 'typescript-parser'; +import { ExtensionContext, Uri } from 'vscode'; import { ExtensionConfig } from '../common/config'; +import { ConfigFactory } from '../common/factories'; import { Logger } from '../common/utilities'; import { CodeActionCreator, MissingImplementationInClassCreator, MissingImportCreator } from './code-actions'; +import { VscodeExtensionConfig } from './config/VscodeExtensionConfig'; import { BaseExtension } from './extensions/BaseExtension'; import { CodeActionExtension } from './extensions/CodeActionExtension'; import { CodeCompletionExtension } from './extensions/CodeCompletionExtension'; @@ -14,38 +16,32 @@ import { ImportResolveExtension } from './extensions/ImportResolveExtension'; import { OrganizeImportsOnSaveExtension } from './extensions/OrganizeImportsOnSaveExtension'; import { iocSymbols } from './IoCSymbols'; import { TypeScriptHero } from './TypeScriptHero'; +import { DeclarationIndexMapper } from './utilities/DeclarationIndexMapper'; import { VscodeLogger } from './utilities/VscodeLogger'; -import { VscodeExtensionConfig } from './VscodeExtensionConfig'; const container = new IoCContainer(); -container.bind(iocSymbols.rootPath).toConstantValue(workspace.rootPath || ''); container.bind(TypeScriptHero).to(TypeScriptHero).inSingletonScope(); -container.bind(iocSymbols.configuration).to(VscodeExtensionConfig).inSingletonScope(); -container - .bind(iocSymbols.declarationIndex) - .toDynamicValue((context: interfaces.Context) => { - const parser = context.container.get(iocSymbols.typescriptParser); - return new DeclarationIndex(parser, context.container.get(iocSymbols.rootPath)); - }) - .inSingletonScope(); + +container.bind(iocSymbols.declarationIndexMapper).to(DeclarationIndexMapper).inSingletonScope(); container .bind(iocSymbols.typescriptParser) - .toDynamicValue(() => { - return new TypescriptParser(); - }) - .inSingletonScope(); + .toConstantValue(new TypescriptParser()); container .bind>(iocSymbols.generatorFactory) .toFactory((context: interfaces.Context) => { - return () => { - const config = context.container.get(iocSymbols.configuration); - return new TypescriptCodeGenerator(config.resolver.generationOptions); + return (resource?: Uri) => { + const configFactory = context.container.get(iocSymbols.configuration); + return new TypescriptCodeGenerator(configFactory(resource).resolver.generationOptions); }; }); +container + .bind>(iocSymbols.configuration) + .toFactory(() => (resource?: Uri) => new VscodeExtensionConfig(resource)); + // Extensions container.bind(iocSymbols.extensions).to(ImportResolveExtension).inSingletonScope(); container.bind(iocSymbols.extensions).to(CodeCompletionExtension).inSingletonScope(); @@ -59,7 +55,7 @@ container .toFactory((context: interfaces.Context) => { return (prefix?: string) => { const extContext = context.container.get(iocSymbols.extensionContext); - const config = context.container.get(iocSymbols.configuration); + const config = context.container.get(iocSymbols.configuration)(); return new VscodeLogger(extContext, config, prefix); }; diff --git a/src/extension/IoCSymbols.ts b/src/extension/IoCSymbols.ts index 193dcec..5fc8ef5 100644 --- a/src/extension/IoCSymbols.ts +++ b/src/extension/IoCSymbols.ts @@ -8,7 +8,6 @@ export const iocSymbols = { loggerFactory: Symbol('loggerFactory'), generatorFactory: Symbol('generatorFactory'), codeActionCreators: Symbol('codeActionCreators'), - declarationIndex: Symbol('declarationIndex'), + declarationIndexMapper: Symbol('declarationIndexMapper'), typescriptParser: Symbol('typescriptParser'), - rootPath: Symbol('rootPath'), }; diff --git a/src/extension/code-actions/MissingImplementationInClassCreator.ts b/src/extension/code-actions/MissingImplementationInClassCreator.ts index f0e1a3b..251f058 100644 --- a/src/extension/code-actions/MissingImplementationInClassCreator.ts +++ b/src/extension/code-actions/MissingImplementationInClassCreator.ts @@ -1,21 +1,16 @@ import { inject, injectable } from 'inversify'; -import { - ClassLikeDeclaration, - DeclarationIndex, - GenericDeclaration, - NamedImport, - TypescriptParser, -} from 'typescript-parser'; -import { Command, Diagnostic, TextDocument } from 'vscode'; +import { ClassLikeDeclaration, GenericDeclaration, NamedImport, TypescriptParser } from 'typescript-parser'; +import { Command, Diagnostic, TextDocument, workspace } from 'vscode'; import { getAbsolutLibraryName } from '../../common/helpers'; import { iocSymbols } from '../IoCSymbols'; +import { DeclarationIndexMapper } from '../utilities/DeclarationIndexMapper'; import { ImplementPolymorphElements, NoopCodeAction } from './CodeAction'; import { CodeActionCreator } from './CodeActionCreator'; /** * Action creator that handles missing implementations in a class. - * + * * @export * @class MissingImplementationInClassCreator * @extends {CodeActionCreator} @@ -24,18 +19,17 @@ import { CodeActionCreator } from './CodeActionCreator'; export class MissingImplementationInClassCreator extends CodeActionCreator { constructor( @inject(iocSymbols.typescriptParser) private parser: TypescriptParser, - @inject(iocSymbols.declarationIndex) private index: DeclarationIndex, - @inject(iocSymbols.rootPath) private rootPath: string, + @inject(iocSymbols.declarationIndexMapper) private indices: DeclarationIndexMapper, ) { super(); } /** * Determines if the given diagnostic can be handled by this creator. - * - * @param {Diagnostic} diagnostic - * @returns {boolean} - * + * + * @param {Diagnostic} diagnostic + * @returns {boolean} + * * @memberof MissingImplementationInClassCreator */ public canHandleDiagnostic(diagnostic: Diagnostic): boolean { @@ -45,19 +39,22 @@ export class MissingImplementationInClassCreator extends CodeActionCreator { /** * Handles the given diagnostic. Must return an array of commands that are given to the light bulb. - * + * * @param {TextDocument} document The commands that are created until now * @param {Command[]} commands The commands that are created until now * @param {Diagnostic} diagnostic The diagnostic to handle - * @returns {Promise} - * + * @returns {Promise} + * * @memberof MissingImplementationInClassCreator */ public async handleDiagnostic(document: TextDocument, commands: Command[], diagnostic: Diagnostic): Promise { const match = /class ['"](.*)['"] incorrectly implements.*['"](.*)['"]\./ig.exec(diagnostic.message) || /non-abstract class ['"](.*)['"].*implement inherited.*from class ['"](.*)['"]\./ig.exec(diagnostic.message); - if (!match) { + const index = this.indices.getIndexForFile(document.uri); + const rootFolder = workspace.getWorkspaceFolder(document.uri); + + if (!match || !index || !rootFolder) { return commands; } @@ -76,9 +73,9 @@ export class MissingImplementationInClassCreator extends CodeActionCreator { o => o instanceof NamedImport && o.specifiers.some(s => s.specifier === specifier), ); const declaration = (parsedDocument.declarations.find(o => o.name === specifier) || - (this.index.declarationInfos.find( + (index.declarationInfos.find( o => o.declaration.name === specifier && - o.from === getAbsolutLibraryName(alreadyImported!.libraryName, document.fileName, this.rootPath), + o.from === getAbsolutLibraryName(alreadyImported!.libraryName, document.fileName, rootFolder.uri.fsPath), ) || { declaration: undefined }).declaration) as (ClassLikeDeclaration & GenericDeclaration) | undefined; if (commands.some((o: Command) => o.title.indexOf(specifier) >= 0)) { diff --git a/src/extension/code-actions/MissingImportCreator.ts b/src/extension/code-actions/MissingImportCreator.ts index 9f490e2..bcdc634 100644 --- a/src/extension/code-actions/MissingImportCreator.ts +++ b/src/extension/code-actions/MissingImportCreator.ts @@ -1,14 +1,14 @@ import { inject, injectable } from 'inversify'; -import { DeclarationIndex } from 'typescript-parser'; import { Command, Diagnostic, TextDocument } from 'vscode'; import { iocSymbols } from '../IoCSymbols'; +import { DeclarationIndexMapper } from '../utilities/DeclarationIndexMapper'; import { AddImportCodeAction, AddMissingImportsCodeAction, NoopCodeAction } from './CodeAction'; import { CodeActionCreator } from './CodeActionCreator'; /** * Action creator that handles missing imports in files. - * + * * @export * @class MissingImportCreator * @extends {CodeActionCreator} @@ -16,17 +16,17 @@ import { CodeActionCreator } from './CodeActionCreator'; @injectable() export class MissingImportCreator extends CodeActionCreator { constructor( - @inject(iocSymbols.declarationIndex) private index: DeclarationIndex, + @inject(iocSymbols.declarationIndexMapper) private indices: DeclarationIndexMapper, ) { super(); } /** * Determines if the given diagnostic can be handled by this creator. - * - * @param {Diagnostic} diagnostic - * @returns {boolean} - * + * + * @param {Diagnostic} diagnostic + * @returns {boolean} + * * @memberof MissingImportCreator */ public canHandleDiagnostic(diagnostic: Diagnostic): boolean { @@ -35,21 +35,23 @@ export class MissingImportCreator extends CodeActionCreator { /** * Handles the given diagnostic. Must return an array of commands that are given to the light bulb. - * + * * @param {TextDocument} document The commands that are created until now * @param {Command[]} commands The commands that are created until now * @param {Diagnostic} diagnostic The diagnostic to handle - * @returns {Promise} - * + * @returns {Promise} + * * @memberof MissingImportCreator */ public async handleDiagnostic(document: TextDocument, commands: Command[], diagnostic: Diagnostic): Promise { const match = /cannot find name ['"](.*)['"]/ig.exec(diagnostic.message); - if (!match) { + const index = this.indices.getIndexForFile(document.uri); + + if (!match || !index) { return commands; } - const infos = this.index.declarationInfos.filter(o => o.declaration.name === match[1]); + const infos = index.declarationInfos.filter(o => o.declaration.name === match[1]); if (infos.length > 0) { for (const info of infos) { commands.push(this.createCommand( @@ -65,7 +67,7 @@ export class MissingImportCreator extends CodeActionCreator { ) { commands.push(this.createCommand( 'Add all missing imports if possible.', - new AddMissingImportsCodeAction(document, this.index), + new AddMissingImportsCodeAction(document, index), )); } } else { diff --git a/src/extension/config/VscodeCodeCompletionConfig.ts b/src/extension/config/VscodeCodeCompletionConfig.ts new file mode 100644 index 0000000..634532e --- /dev/null +++ b/src/extension/config/VscodeCodeCompletionConfig.ts @@ -0,0 +1,30 @@ +import { Uri, workspace, WorkspaceConfiguration } from 'vscode'; + +import { CodeCompletionConfig } from '../../common/config'; + +const sectionKey = 'typescriptHero.codeCompletion'; + +/** + * Configuration interface for the code outline feature. + * + * @class VscodeCodeCompletionConfig + * @implements {CodeCompletionConfig} + */ +export class VscodeCodeCompletionConfig implements CodeCompletionConfig { + private get workspaceSection(): WorkspaceConfiguration { + return workspace.getConfiguration(sectionKey, this.resource); + } + + /** + * Defines the used completion sort mode (i.e. if the completions should be sorted to the bottom of the list). + * + * @readonly + * @type {'default'|'bottom'} + * @memberof VscodeCodeCompletionConfig + */ + public get completionSortMode(): 'default' | 'bottom' { + return this.workspaceSection.get<'default' | 'bottom'>('completionSortMode', 'default'); + } + + constructor(public readonly resource?: Uri) { } +} diff --git a/src/extension/config/VscodeCodeOutlineConfig.ts b/src/extension/config/VscodeCodeOutlineConfig.ts new file mode 100644 index 0000000..4d1db04 --- /dev/null +++ b/src/extension/config/VscodeCodeOutlineConfig.ts @@ -0,0 +1,30 @@ +import { Uri, workspace, WorkspaceConfiguration } from 'vscode'; + +import { CodeOutlineConfig } from '../../common/config'; + +const sectionKey = 'typescriptHero.codeOutline'; + +/** + * Configuration interface for the code outline feature. + * + * @class VscodeCodeOutlineConfig + * @implements {CodeOutlineConfig} + */ +export class VscodeCodeOutlineConfig implements CodeOutlineConfig { + private get workspaceSection(): WorkspaceConfiguration { + return workspace.getConfiguration(sectionKey, this.resource); + } + + /** + * Defined if the code outline feature is enabled or not. + * + * @readonly + * @type {boolean} + * @memberof VscodeCodeOutlineConfig + */ + public get outlineEnabled(): boolean { + return this.workspaceSection.get('enabled', true); + } + + constructor(public readonly resource?: Uri) { } +} diff --git a/src/extension/config/VscodeExtensionConfig.ts b/src/extension/config/VscodeExtensionConfig.ts new file mode 100644 index 0000000..35ac546 --- /dev/null +++ b/src/extension/config/VscodeExtensionConfig.ts @@ -0,0 +1,86 @@ +import { injectable } from 'inversify'; +import { Uri, workspace, WorkspaceConfiguration } from 'vscode'; + +import { CodeCompletionConfig, CodeOutlineConfig, ExtensionConfig, ResolverConfig } from '../../common/config'; +import { VscodeCodeCompletionConfig } from './VscodeCodeCompletionConfig'; +import { VscodeCodeOutlineConfig } from './VscodeCodeOutlineConfig'; +import { VscodeResolverConfig } from './VscodeResolverConfig'; + +const sectionKey = 'typescriptHero'; + +/** + * Configuration class for TypeScript Hero + * Contains all exposed config endpoints. + * + * @export + * @class VscodeExtensionConfig + */ +@injectable() +export class VscodeExtensionConfig implements ExtensionConfig { + public readonly possibleLanguages: string[] = [ + 'typescript', + 'typescriptreact', + 'javascript', + 'javascriptreact', + ]; + + private resolverConfig: ResolverConfig; + private codeOutlineConfig: CodeOutlineConfig; + private codeCompletionConfig: CodeCompletionConfig; + + private get workspaceSection(): WorkspaceConfiguration { + return workspace.getConfiguration(sectionKey, this.resource); + } + + /** + * The actual log level. + * + * @readonly + * @type {string} + * @memberof VscodeExtensionConfig + */ + public get verbosity(): string { + return this.workspaceSection.get('verbosity', 'Warning'); + } + + /** + * Configuration object for the resolver extension. + * + * @readonly + * @type {ResolverConfig} + * @memberof VscodeExtensionConfig + */ + public get resolver(): ResolverConfig { + return this.resolverConfig; + } + + /** + * Configuration object for the code outline extension. + * + * @readonly + * @type {CodeOutlineConfig} + * @memberof VscodeExtensionConfig + */ + public get codeOutline(): CodeOutlineConfig { + return this.codeOutlineConfig; + } + + /** + * Configuration object for the code completion extension. + * + * @readonly + * @type {CodeCompletionConfig} + * @memberof VscodeExtensionConfig + */ + public get codeCompletion(): CodeCompletionConfig { + return this.codeCompletionConfig; + } + + constructor(public readonly resource?: Uri) { + this.codeCompletionConfig = new VscodeCodeCompletionConfig(resource); + this.codeOutlineConfig = new VscodeCodeOutlineConfig(resource); + this.resolverConfig = new VscodeResolverConfig(resource); + } +} + + diff --git a/src/extension/VscodeExtensionConfig.ts b/src/extension/config/VscodeResolverConfig.ts similarity index 60% rename from src/extension/VscodeExtensionConfig.ts rename to src/extension/config/VscodeResolverConfig.ts index 23ac1dc..9e08717 100644 --- a/src/extension/VscodeExtensionConfig.ts +++ b/src/extension/config/VscodeResolverConfig.ts @@ -1,83 +1,20 @@ -import { injectable } from 'inversify'; import { TypescriptGenerationOptions } from 'typescript-parser'; -import { workspace, WorkspaceConfiguration } from 'vscode'; +import { Uri, workspace, WorkspaceConfiguration } from 'vscode'; -import { ExtensionConfig, ResolverConfig } from '../common/config'; -import { CodeOutlineConfig } from '../common/config/CodeOutlineConfig'; -import { ResolverMode } from '../common/enums'; -import { ImportGroup, ImportGroupSetting, ImportGroupSettingParser, RemainImportGroup } from './import-grouping'; +import { ResolverConfig } from '../../common/config'; +import { ResolverMode } from '../../common/enums'; +import { ImportGroup, ImportGroupSetting, ImportGroupSettingParser, RemainImportGroup } from '../import-grouping'; -const sectionKey = 'typescriptHero'; - -/** - * Configuration class for TypeScript Hero - * Contains all exposed config endpoints. - * - * @export - * @class VscodeExtensionConfig - */ -@injectable() -export class VscodeExtensionConfig implements ExtensionConfig { - private resolverConfig: ResolverConfig = new VscodeResolverConfig(); - private codeOutlineConfig: CodeOutlineConfig = new VscodeCodeOutlineConfig(); - - private get workspaceSection(): WorkspaceConfiguration { - return workspace.getConfiguration(sectionKey); - } - - /** - * The actual log level. - * - * @readonly - * @type {string} - * @memberof VscodeExtensionConfig - */ - public get verbosity(): string { - return this.workspaceSection.get('verbosity') || 'Warning'; - } - - /** - * Configuration object for the resolver extension. - * - * @readonly - * @type {ResolverConfig} - * @memberof VscodeExtensionConfig - */ - public get resolver(): ResolverConfig { - return this.resolverConfig; - } - - /** - * Configuration object for the code outline extension. - * - * @readonly - * @type {CodeOutlineConfig} - * @memberof VscodeExtensionConfig - */ - public get codeOutline(): CodeOutlineConfig { - return this.codeOutlineConfig; - } - - /** - * Completion sort mode - * - * @readonly - * @type {'default'|'bottom'} - * @memberof VscodeExtensionConfig - */ - public get completionSortMode(): 'default' | 'bottom' { - return this.workspaceSection.get<'default' | 'bottom'>('completionSortMode') || 'default'; - } -} +const sectionKey = 'typescriptHero.resolver'; /** * Configuration class for the resolver extension. * * @class VscodeResolverConfig */ -class VscodeResolverConfig implements ResolverConfig { +export class VscodeResolverConfig implements ResolverConfig { private get workspaceSection(): WorkspaceConfiguration { - return workspace.getConfiguration(sectionKey); + return workspace.getConfiguration(sectionKey, this.resource); } /** @@ -89,8 +26,7 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get insertSpaceBeforeAndAfterImportBraces(): boolean { - const value = this.workspaceSection.get('resolver.insertSpaceBeforeAndAfterImportBraces'); - return value !== undefined ? value : true; + return this.workspaceSection.get('insertSpaceBeforeAndAfterImportBraces', true); } /** @@ -102,8 +38,7 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get insertSemicolons(): boolean { - const value = this.workspaceSection.get('resolver.insertSemicolons'); - return value !== undefined ? value : true; + return this.workspaceSection.get('insertSemicolons', true); } /** @@ -114,7 +49,7 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get stringQuoteStyle(): string { - return this.workspaceSection.get('resolver.stringQuoteStyle') || `'`; + return this.workspaceSection.get('stringQuoteStyle', `'`); } /** @@ -126,11 +61,14 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get ignorePatterns(): string[] { - return this.workspaceSection.get('resolver.ignorePatterns') || [ - 'build', - 'out', - 'dist', - ]; + return this.workspaceSection.get( + 'ignorePatterns', + [ + 'build', + 'out', + 'dist', + ], + ); } /** @@ -141,7 +79,7 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get multiLineWrapThreshold(): number { - return this.workspaceSection.get('resolver.multiLineWrapThreshold') || 125; + return this.workspaceSection.get('multiLineWrapThreshold', 125); } /** @@ -158,8 +96,7 @@ class VscodeResolverConfig implements ResolverConfig { * } from 'whatever'; */ public get multiLineTrailingComma(): boolean { - const value = this.workspaceSection.get('resolver.multiLineTrailingComma'); - return value !== undefined ? value : true; + return this.workspaceSection.get('multiLineTrailingComma', true); } /** @@ -170,8 +107,7 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof ResolverConfig */ public get disableImportSorting(): boolean { - const value = this.workspaceSection.get('resolver.disableImportsSorting'); - return value !== undefined ? value : false; + return this.workspaceSection.get('disableImportsSorting', false); } /** @@ -182,8 +118,7 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof ResolverConfig */ public get disableImportRemovalOnOrganize(): boolean { - const value = this.workspaceSection.get('resolver.disableImportRemovalOnOrganize'); - return value !== undefined ? value : false; + return this.workspaceSection.get('disableImportRemovalOnOrganize', false); } /** @@ -194,7 +129,7 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get tabSize(): number { - return workspace.getConfiguration().get('editor.tabSize') || 4; + return workspace.getConfiguration().get('editor.tabSize', 4); } /** @@ -205,7 +140,7 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get ignoreImportsForOrganize(): string[] { - return this.workspaceSection.get('resolver.ignoreImportsForOrganize') || []; + return this.workspaceSection.get('ignoreImportsForOrganize', []); } /** @@ -215,7 +150,7 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get importGroups(): ImportGroup[] { - const groups = this.workspaceSection.get('resolver.importGroups'); + const groups = this.workspaceSection.get('importGroups'); let importGroups: ImportGroup[] = []; try { @@ -253,14 +188,14 @@ class VscodeResolverConfig implements ResolverConfig { } /** - * Current mode of the resolver. + * Current mode of the * * @readonly * @type {ResolverMode} * @memberof VscodeResolverConfig */ public get resolverMode(): ResolverMode { - const mode = this.workspaceSection.get('resolver.resolverMode', 'TypeScript'); + const mode = this.workspaceSection.get('resolverMode', 'TypeScript'); return ResolverMode[mode] || ResolverMode.TypeScript; } @@ -330,8 +265,8 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get organizeOnSave(): boolean { - const typescriptHeroValue = this.workspaceSection.get('resolver.organizeOnSave', true); - const editorValue = workspace.getConfiguration().get('editor.formatOnSave', false); + const typescriptHeroValue = this.workspaceSection.get('organizeOnSave', false); + const editorValue = workspace.getConfiguration('editor', this.resource).get('formatOnSave', false); return typescriptHeroValue && editorValue; } @@ -344,30 +279,8 @@ class VscodeResolverConfig implements ResolverConfig { * @memberof VscodeResolverConfig */ public get promptForSpecifiers(): boolean { - return this.workspaceSection.get('resolver.promptForSpecifiers', false); + return this.workspaceSection.get('promptForSpecifiers', false); } -} -/** - * Configuration interface for the code outline feature. - * - * @class VscodeCodeOutlineConfig - * @implements {CodeOutlineConfig} - */ -class VscodeCodeOutlineConfig implements CodeOutlineConfig { - private get workspaceSection(): WorkspaceConfiguration { - return workspace.getConfiguration(sectionKey); - } - - /** - * Defined if the code outline feature is enabled or not. - * - * @readonly - * @type {boolean} - * @memberof VscodeCodeOutlineConfig - */ - public get outlineEnabled(): boolean { - const value = this.workspaceSection.get('codeOutline.enabled'); - return value !== undefined ? value : true; - } + constructor(public readonly resource?: Uri) { } } diff --git a/src/extension/extensions/CodeActionExtension.ts b/src/extension/extensions/CodeActionExtension.ts index 94580f2..2634847 100644 --- a/src/extension/extensions/CodeActionExtension.ts +++ b/src/extension/extensions/CodeActionExtension.ts @@ -1,5 +1,4 @@ import { inject, injectable, multiInject } from 'inversify'; -import { DeclarationIndex } from 'typescript-parser'; import { CancellationToken, CodeActionContext, @@ -17,13 +16,14 @@ import { Logger, LoggerFactory } from '../../common/utilities'; import { CodeAction } from '../code-actions/CodeAction'; import { CodeActionCreator } from '../code-actions/CodeActionCreator'; import { iocSymbols } from '../IoCSymbols'; +import { DeclarationIndexMapper } from '../utilities/DeclarationIndexMapper'; import { BaseExtension } from './BaseExtension'; /** * Provider instance that is responsible for the "light bulb" feature. - * It provides actions to take when errors occur in the current document (such as missing imports or + * It provides actions to take when errors occur in the current document (such as missing imports or * non implemented interfaces.). - * + * * @export * @class CodeActionExtension * @implements {CodeActionProvider} @@ -36,7 +36,7 @@ export class CodeActionExtension extends BaseExtension implements CodeActionProv @inject(iocSymbols.extensionContext) context: ExtensionContext, @inject(iocSymbols.loggerFactory) loggerFactory: LoggerFactory, @multiInject(iocSymbols.codeActionCreators) private actionCreators: CodeActionCreator[], - @inject(iocSymbols.declarationIndex) private index: DeclarationIndex, + @inject(iocSymbols.declarationIndexMapper) private indices: DeclarationIndexMapper, ) { super(context); this.logger = loggerFactory('CodeActionExtension'); @@ -44,7 +44,7 @@ export class CodeActionExtension extends BaseExtension implements CodeActionProv /** * Initialized the extension. Registers the commands and other disposables to the context. - * + * * @memberof ImportResolveExtension */ public initialize(): void { @@ -60,7 +60,7 @@ export class CodeActionExtension extends BaseExtension implements CodeActionProv /** * Disposes the extension. - * + * * @memberof ImportResolveExtension */ public dispose(): void { @@ -69,13 +69,13 @@ export class CodeActionExtension extends BaseExtension implements CodeActionProv /** * Provides the commands to execute for a given problem. - * + * * @param {TextDocument} document * @param {Range} range * @param {CodeActionContext} context * @param {CancellationToken} token * @returns {Promise} - * + * * @memberof CodeActionExtension */ public async provideCodeActions( @@ -84,7 +84,8 @@ export class CodeActionExtension extends BaseExtension implements CodeActionProv context: CodeActionContext, _token: CancellationToken, ): Promise { - if (!this.index.indexReady) { + const index = this.indices.getIndexForFile(document.uri); + if (!index || !index.indexReady) { return []; } @@ -102,11 +103,11 @@ export class CodeActionExtension extends BaseExtension implements CodeActionProv /** * Executes a code action. If the result is false, a warning is shown. - * + * * @private * @param {CodeAction} codeAction * @returns {Promise} - * + * * @memberof CodeFixExtension */ private async executeCodeAction(codeAction: CodeAction | undefined): Promise { diff --git a/src/extension/extensions/CodeCompletionExtension.ts b/src/extension/extensions/CodeCompletionExtension.ts index f5eff50..4a01f6d 100644 --- a/src/extension/extensions/CodeCompletionExtension.ts +++ b/src/extension/extensions/CodeCompletionExtension.ts @@ -1,5 +1,5 @@ import { inject, injectable } from 'inversify'; -import { DeclarationIndex, DeclarationInfo, TypescriptParser } from 'typescript-parser'; +import { DeclarationInfo, TypescriptParser } from 'typescript-parser'; import { CancellationToken, commands, @@ -9,21 +9,21 @@ import { languages, Position, TextDocument, - Disposable, workspace, } from 'vscode'; -import { ExtensionConfig } from '../../common/config'; +import { ConfigFactory } from '../../common/factories'; import { getDeclarationsFilteredByImports } from '../../common/helpers'; import { Logger, LoggerFactory } from '../../common/utilities'; import { iocSymbols } from '../IoCSymbols'; import { ImportManager } from '../managers/ImportManager'; +import { DeclarationIndexMapper } from '../utilities/DeclarationIndexMapper'; import { getItemKind } from '../utilities/utilityFunctions'; import { BaseExtension } from './BaseExtension'; /** * Extension that provides code completion for typescript files. Uses the calculated index to provide information. - * + * * @export * @class CodeCompletionExtension * @extends {BaseExtension} @@ -32,15 +32,13 @@ import { BaseExtension } from './BaseExtension'; @injectable() export class CodeCompletionExtension extends BaseExtension implements CompletionItemProvider { private logger: Logger; - private languageRegisters: Disposable[] = []; constructor( @inject(iocSymbols.extensionContext) context: ExtensionContext, @inject(iocSymbols.loggerFactory) loggerFactory: LoggerFactory, @inject(iocSymbols.typescriptParser) private parser: TypescriptParser, - @inject(iocSymbols.declarationIndex) private index: DeclarationIndex, - @inject(iocSymbols.rootPath) private rootPath: string, - @inject(iocSymbols.configuration) private config: ExtensionConfig, + @inject(iocSymbols.declarationIndexMapper) private indices: DeclarationIndexMapper, + @inject(iocSymbols.configuration) private config: ConfigFactory, ) { super(context); this.logger = loggerFactory('CodeCompletionExtension'); @@ -48,27 +46,14 @@ export class CodeCompletionExtension extends BaseExtension implements Completion /** * Initialized the extension. Registers the commands and other disposables to the context. - * + * * @memberof CodeCompletionExtension */ public initialize(): void { - for (const lang of this.config.resolver.resolverModeLanguages) { - this.languageRegisters.push(languages.registerCompletionItemProvider(lang, this)); + for (const lang of this.config().possibleLanguages) { + this.context.subscriptions.push(languages.registerCompletionItemProvider(lang, this)); } - this.context.subscriptions.push(workspace.onDidChangeConfiguration(() => { - if (this.languageRegisters.length !== this.config.resolver.resolverModeLanguages.length) { - this.logger.info('ResolverMode has changed, registering to new configuration languages'); - for (const register of this.languageRegisters) { - register.dispose(); - } - this.languageRegisters = []; - for (const lang of this.config.resolver.resolverModeLanguages) { - this.languageRegisters.push(languages.registerCompletionItemProvider(lang, this)); - } - } - })); - this.context.subscriptions.push( commands.registerCommand( 'typescriptHero.codeCompletion.executeIntellisenseItem', @@ -82,24 +67,21 @@ export class CodeCompletionExtension extends BaseExtension implements Completion /** * Disposes the extension. - * + * * @memberof CodeCompletionExtension */ public dispose(): void { - for (const register of this.languageRegisters) { - register.dispose(); - } this.logger.info('Disposed'); } /** * Provides completion items for a given position in the given document. - * - * @param {TextDocument} document - * @param {Position} position - * @param {CancellationToken} token - * @returns {Promise<(CompletionItem[] | null)>} - * + * + * @param {TextDocument} document + * @param {Position} position + * @param {CancellationToken} token + * @returns {Promise<(CompletionItem[] | null)>} + * * @memberof CodeCompletionExtension */ public async provideCompletionItems( @@ -107,7 +89,15 @@ export class CodeCompletionExtension extends BaseExtension implements Completion position: Position, token: CancellationToken, ): Promise { - if (!this.index.indexReady) { + const index = this.indices.getIndexForFile(document.uri); + const config = this.config(document.uri); + const rootFolder = workspace.getWorkspaceFolder(document.uri); + + if (!index || + !index.indexReady || + !config.resolver.resolverModeLanguages.some(lng => lng === document.languageId) || + !rootFolder + ) { return null; } @@ -123,7 +113,7 @@ export class CodeCompletionExtension extends BaseExtension implements Completion if (!searchWord || token.isCancellationRequested || - !this.index.indexReady || + !index.indexReady || (lineText.substring(0, position.character).match(/["'`]/g) || []).length % 2 === 1 || lineText.match(/^\s*(\/\/|\/\*\*|\*\/|\*)/g) || lineText.startsWith('import ') || @@ -135,10 +125,10 @@ export class CodeCompletionExtension extends BaseExtension implements Completion const parsed = await this.parser.parseSource(document.getText()); const declarations = getDeclarationsFilteredByImports( - this.index.declarationInfos, + index.declarationInfos, document.fileName, parsed.imports, - this.rootPath, + rootFolder.uri.fsPath, ) .filter(o => !parsed.declarations.some(d => d.name === o.declaration.name)) .filter(o => !parsed.usages.some(d => d === o.declaration.name)); @@ -155,7 +145,7 @@ export class CodeCompletionExtension extends BaseExtension implements Completion title: 'Execute intellisense insert', command: 'typescriptHero.codeCompletion.executeIntellisenseItem', }; - if (this.config.completionSortMode === 'bottom') { + if (config.codeCompletion.completionSortMode === 'bottom') { item.sortText = `9999-${declaration.declaration.name}`; } items.push(item); @@ -166,11 +156,11 @@ export class CodeCompletionExtension extends BaseExtension implements Completion /** * Executes a intellisense item that provided a document and a declaration to add. * Does make the calculation of the text edits async. - * + * * @private - * @param {TextDocument} document - * @param {DeclarationInfo} declaration - * @returns {Promise} + * @param {TextDocument} document + * @param {DeclarationInfo} declaration + * @returns {Promise} * @memberof CodeCompletionExtension */ private async executeIntellisenseItem(document: TextDocument, declaration: DeclarationInfo): Promise { diff --git a/src/extension/extensions/DocumentSymbolStructureExtension.ts b/src/extension/extensions/DocumentSymbolStructureExtension.ts index 760b9ad..8332f6d 100644 --- a/src/extension/extensions/DocumentSymbolStructureExtension.ts +++ b/src/extension/extensions/DocumentSymbolStructureExtension.ts @@ -13,7 +13,7 @@ import { workspace, } from 'vscode'; -import { ExtensionConfig } from '../../common/config'; +import { ConfigFactory } from '../../common/factories'; import { Logger, LoggerFactory } from '../../common/utilities'; import { iocSymbols } from '../IoCSymbols'; import { BaseStructureTreeItem } from '../provider-items/document-structure/BaseStructureTreeItem'; @@ -42,7 +42,7 @@ export class DocumentSymbolStructureExtension extends BaseExtension implements T constructor( @inject(iocSymbols.extensionContext) context: ExtensionContext, @inject(iocSymbols.loggerFactory) loggerFactory: LoggerFactory, - @inject(iocSymbols.configuration) private config: ExtensionConfig, + @inject(iocSymbols.configuration) private config: ConfigFactory, @inject(iocSymbols.typescriptParser) private parser: TypescriptParser, ) { super(context); @@ -85,15 +85,17 @@ export class DocumentSymbolStructureExtension extends BaseExtension implements T } public async getChildren(element?: BaseStructureTreeItem): Promise> { - if (!this.config.codeOutline.outlineEnabled) { - return [new DisabledStructureTreeItem()]; - } - if (!window.activeTextEditor) { return []; } - if (!this.config.resolver.resolverModeLanguages.some( + const config = this.config(window.activeTextEditor.document.uri); + + if (!config.codeOutline.outlineEnabled) { + return [new DisabledStructureTreeItem()]; + } + + if (!config.resolver.resolverModeLanguages.some( lang => lang === window.activeTextEditor!.document.languageId, )) { return [new NotParseableStructureTreeItem()]; diff --git a/src/extension/extensions/ImportResolveExtension.ts b/src/extension/extensions/ImportResolveExtension.ts index abff4db..f906dd6 100644 --- a/src/extension/extensions/ImportResolveExtension.ts +++ b/src/extension/extensions/ImportResolveExtension.ts @@ -1,122 +1,18 @@ -import { existsSync } from 'fs'; import { inject, injectable } from 'inversify'; -import { join } from 'path'; -import { DeclarationIndex, DeclarationInfo, FileChanges, TypescriptParser } from 'typescript-parser'; -import { - commands, - ExtensionContext, - FileSystemWatcher, - ProgressLocation, - StatusBarAlignment, - StatusBarItem, - Uri, - window, - workspace, -} from 'vscode'; - -import { ExtensionConfig } from '../../common/config'; -import { ResolverMode } from '../../common/enums'; +import { DeclarationInfo, TypescriptParser } from 'typescript-parser'; +import { commands, ExtensionContext, StatusBarAlignment, StatusBarItem, window, workspace } from 'vscode'; + import { getDeclarationsFilteredByImports } from '../../common/helpers'; import { ResolveQuickPickItem } from '../../common/quick-pick-items'; import { Logger, LoggerFactory } from '../../common/utilities'; import { iocSymbols } from '../IoCSymbols'; import { ImportManager } from '../managers'; +import { DeclarationIndexMapper } from '../utilities/DeclarationIndexMapper'; import { BaseExtension } from './BaseExtension'; type DeclarationsForImportOptions = { cursorSymbol: string, documentSource: string, documentPath: string }; type MissingDeclarationsForFileOptions = { documentSource: string, documentPath: string }; -/** - * Compares the ignorepatterns (if they have the same elements ignored). - * - * @param {string[]} local - * @param {string[]} config - * @returns {boolean} - */ -function compareIgnorePatterns(local: string[], config: string[]): boolean { - if (local.length !== config.length) { - return false; - } - const localSorted = local.sort(); - const configSorted = config.sort(); - - for (let x = 0; x < configSorted.length; x += 1) { - if (configSorted[x] !== localSorted[x]) { - return false; - } - } - - return true; -} - -/** - * Search for typescript / typescript react files in the workspace and return the path to them. - * This is needed for the initial load of the index. - * - * @export - * @param {ExtensionConfig} config - * @returns {Promise} - */ -export async function findFiles(config: ExtensionConfig, rootPath: string): Promise { - const searches: PromiseLike[] = [ - workspace.findFiles( - `{${config.resolver.resolverModeFileGlobs.join(',')}}`, - '{**/node_modules/**,**/typings/**}', - ), - ]; - - // TODO: check the package json and index javascript file in node_modules (?) - - let globs: string[] = []; - let ignores = ['**/typings/**']; - const excludePatterns = config.resolver.ignorePatterns; - - if (rootPath && existsSync(join(rootPath, 'package.json'))) { - const packageJson = require(join(rootPath, 'package.json')); - if (packageJson['dependencies']) { - globs = globs.concat( - Object.keys(packageJson['dependencies']).filter(o => excludePatterns.indexOf(o) < 0) - .map(o => `**/node_modules/${o}/**/*.d.ts`), - ); - ignores = ignores.concat( - Object.keys(packageJson['dependencies']).filter(o => excludePatterns.indexOf(o) < 0) - .map(o => `**/node_modules/${o}/node_modules/**`), - ); - } - if (packageJson['devDependencies']) { - globs = globs.concat( - Object.keys(packageJson['devDependencies']).filter(o => excludePatterns.indexOf(o) < 0) - .map(o => `**/node_modules/${o}/**/*.d.ts`), - ); - ignores = ignores.concat( - Object.keys(packageJson['devDependencies']).filter(o => excludePatterns.indexOf(o) < 0) - .map(o => `**/node_modules/${o}/node_modules/**`), - ); - } - } else { - globs.push('**/node_modules/**/*.d.ts'); - } - searches.push( - workspace.findFiles(`{${globs.join(',')}}`, `{${ignores.join(',')}}`), - ); - - searches.push( - workspace.findFiles('**/typings/**/*.d.ts', '**/node_modules/**'), - ); - - let uris = await Promise.all(searches); - - uris = uris.map((o, idx) => idx === 0 ? - o.filter( - f => f.fsPath - .replace(rootPath || '', '') - .split(/\\|\//) - .every(p => excludePatterns.indexOf(p) < 0)) : - o, - ); - return uris.reduce((all, cur) => all.concat(cur), []).map(o => o.fsPath); -} - const resolverOk = 'TSH Resolver $(check)'; const resolverSyncing = 'TSH Resolver $(sync)'; const resolverErr = 'TSH Resolver $(flame)'; @@ -124,7 +20,7 @@ const resolverErr = 'TSH Resolver $(flame)'; /** * Extension that resolves imports. Contains various actions to add imports to a document, add missing * imports and organize imports. Also can rebuild the symbol cache. - * + * * @export * @class ImportResolveExtension * @extends {BaseExtension} @@ -133,17 +29,12 @@ const resolverErr = 'TSH Resolver $(flame)'; export class ImportResolveExtension extends BaseExtension { private logger: Logger; private statusBarItem: StatusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, 4); - private ignorePatterns: string[]; - private fileWatcher: FileSystemWatcher; - private actualMode: ResolverMode; constructor( @inject(iocSymbols.extensionContext) context: ExtensionContext, @inject(iocSymbols.loggerFactory) loggerFactory: LoggerFactory, - @inject(iocSymbols.configuration) private config: ExtensionConfig, @inject(iocSymbols.typescriptParser) private parser: TypescriptParser, - @inject(iocSymbols.declarationIndex) private index: DeclarationIndex, - @inject(iocSymbols.rootPath) private rootPath: string, + @inject(iocSymbols.declarationIndexMapper) private indices: DeclarationIndexMapper, ) { super(context); this.logger = loggerFactory('ImportResolveExtension'); @@ -151,174 +42,61 @@ export class ImportResolveExtension extends BaseExtension { /** * Initialized the extension. Registers the commands and other disposables to the context. - * + * * @memberof ImportResolveExtension */ public initialize(): void { - this.actualMode = this.config.resolver.resolverMode; - this.ignorePatterns = this.config.resolver.ignorePatterns; - - this.fileWatcher = workspace.createFileSystemWatcher( - `{${this.config.resolver.resolverModeFileGlobs.join(',')},**/package.json,**/typings.json}`, - ); - this.context.subscriptions.push(this.statusBarItem); - this.context.subscriptions.push(this.fileWatcher); this.statusBarItem.text = resolverOk; - this.statusBarItem.tooltip = - `Click to manually reindex all files; Actual mode: ${ResolverMode[this.config.resolver.resolverMode]}`; + this.statusBarItem.tooltip = 'Click to manually reindex all files'; this.statusBarItem.command = 'typescriptHero.resolve.rebuildCache'; + this.context.subscriptions.push(this.indices.onStartIndexing(() => { + this.statusBarItem.text = resolverSyncing; + })); + this.context.subscriptions.push(this.indices.onFinishIndexing(() => { + this.statusBarItem.text = resolverOk; + })); + this.context.subscriptions.push(this.indices.onIndexingError(() => { + this.statusBarItem.text = resolverErr; + })); this.statusBarItem.show(); this.commandRegistrations(); - this.context.subscriptions.push(workspace.onDidChangeConfiguration(() => { - let build = false; - if (!compareIgnorePatterns(this.ignorePatterns, this.config.resolver.ignorePatterns)) { - this.logger.info('The typescriptHero.resolver.ignorePatterns setting was modified, reload the index.'); - this.ignorePatterns = this.config.resolver.ignorePatterns; - build = true; - } - if (this.actualMode !== this.config.resolver.resolverMode) { - this.logger.info('The typescriptHero.resolver.resolverMode setting was modified, reload the index.'); - this.statusBarItem.tooltip = - `Click to manually reindex all files; Actual mode: ${ResolverMode[this.config.resolver.resolverMode]}`; - this.actualMode = this.config.resolver.resolverMode; - build = true; - } - if (build) { - this.buildIndex(); - } - })); - - let timeout: NodeJS.Timer | undefined; - let events: FileChanges | undefined; - - const fileWatcherEvent = (event: string, uri: Uri) => { - if (timeout) { - clearTimeout(timeout); - } - if (!events) { - events = { - created: [], - updated: [], - deleted: [], - }; - } - events[event].push(uri.fsPath); - - timeout = setTimeout( - () => { - if (events) { - this.rebuildForFileChanges(events); - events = undefined; - } - }, - 500, - ); - }; - - this.fileWatcher.onDidCreate(uri => fileWatcherEvent('created', uri)); - this.fileWatcher.onDidChange(uri => fileWatcherEvent('updated', uri)); - this.fileWatcher.onDidDelete(uri => fileWatcherEvent('deleted', uri)); - - this.buildIndex(); - this.logger.info('Initialized'); } /** * Disposes the extension. - * + * * @memberof ImportResolveExtension */ public dispose(): void { this.logger.info('Disposed'); } - /** - * Instructs the index to build an index for the found files (actually searches for all files in the - * current workspace). - * - * @private - * @returns {Promise} - * - * @memberof ImportResolveExtension - */ - private async buildIndex(): Promise { - await this.abstractIndexFunction('Create index.', async () => { - const files = await findFiles(this.config, this.rootPath); - this.logger.info(`Calculating index for ${files.length} files.`); - await this.index.buildIndex(files); - }); - } - - /** - * Instructs the index to rebuild the partial index for the changed files. - * - * @private - * @param {FileChanges} changes - * @returns {Promise} - * @memberof ImportResolveExtension - */ - private async rebuildForFileChanges(changes: FileChanges): Promise { - this.abstractIndexFunction('Reindex changes.', async () => { - await this.index.reindexForChanges(changes); - }); - } - - /** - * Abstracts the build and rebuild functions to be just one call withProgress. - * - * @private - * @param {string} title - * @param {() => Promise} func - * @returns {Promise} - * @memberof ImportResolveExtension - */ - private async abstractIndexFunction(title: string, func: () => Promise): Promise { - await window.withProgress( - { - title, - location: ProgressLocation.Window, - }, - async (progress) => { - this.logger.info('(Re-)Calculating index.'); - progress.report({ message: '(Re-)Calculating index.' }); - this.statusBarItem.text = resolverSyncing; - - try { - await func(); - this.logger.info('(Re-)Calculate finished.'); - progress.report({ message: '(Re-)Calculate finished.' }); - this.statusBarItem.text = resolverOk; - } catch (e) { - this.logger.error('There was an error during the index (re)calculation.', e); - progress.report({ message: 'There was an error during the index (re)calculation.' }); - this.statusBarItem.text = resolverErr; - } - }, - ); - } - /** * Add an import from the whole list. Calls the vscode gui, where the user can * select a symbol to import. - * + * * @private * @returns {Promise} - * + * * @memberof ResolveExtension */ private async addImport(): Promise { - if (!this.index.indexReady) { + if (!window.activeTextEditor) { + return; + } + const index = this.indices.getIndexForFile(window.activeTextEditor.document.uri); + if (!index || !index.indexReady) { this.showCacheWarning(); return; } try { const selectedItem = await window.showQuickPick( - this.index.declarationInfos.map(o => new ResolveQuickPickItem(o)), + index.declarationInfos.map(o => new ResolveQuickPickItem(o)), { placeHolder: 'Add import to document:' }, ); if (selectedItem) { @@ -334,17 +112,18 @@ export class ImportResolveExtension extends BaseExtension { /** * Add an import from the whole list. Calls the vscode gui, where the user can * select a symbol to import. - * + * * @private * @returns {Promise} - * + * * @memberof ImportResolveExtension */ private async addImportUnderCursor(): Promise { if (!window.activeTextEditor) { return; } - if (!this.index.indexReady) { + const index = this.indices.getIndexForFile(window.activeTextEditor.document.uri); + if (!index || !index.indexReady) { this.showCacheWarning(); return; } @@ -384,17 +163,18 @@ export class ImportResolveExtension extends BaseExtension { /** * Adds all missing imports to the actual document if possible. If multiple declarations are found, * a quick pick list is shown to the user and he needs to decide, which import to use. - * + * * @private * @returns {Promise} - * + * * @memberof ImportResolveExtension */ private async addMissingImports(): Promise { if (!window.activeTextEditor) { return; } - if (!this.index.indexReady) { + const index = this.indices.getIndexForFile(window.activeTextEditor.document.uri); + if (!index || !index.indexReady) { this.showCacheWarning(); return; } @@ -417,10 +197,10 @@ export class ImportResolveExtension extends BaseExtension { /** * Organizes the imports of the actual document. Sorts and formats them correctly. - * + * * @private * @returns {Promise} - * + * * @memberof ImportResolveExtension */ private async organizeImports(): Promise { @@ -438,11 +218,11 @@ export class ImportResolveExtension extends BaseExtension { /** * Effectifely adds an import quick pick item to a document - * + * * @private * @param {DeclarationInfo} declaration * @returns {Promise} - * + * * @memberof ImportResolveExtension */ private async addImportToDocument(declaration: DeclarationInfo): Promise { @@ -455,10 +235,10 @@ export class ImportResolveExtension extends BaseExtension { /** * Returns the string under the cursor. - * + * * @private * @returns {string} - * + * * @memberof ImportResolveExtension */ private getSymbolUnderCursor(): string { @@ -474,9 +254,9 @@ export class ImportResolveExtension extends BaseExtension { /** * Shows a user warning if the resolve index is not ready yet. - * + * * @private - * + * * @memberof ImportResolveExtension */ private showCacheWarning(): void { @@ -486,25 +266,35 @@ export class ImportResolveExtension extends BaseExtension { /** * Calculates the possible imports for a given document source with a filter for the given symbol. * Returns a list of declaration infos that may be used for select picker or something. - * + * * @private * @param {DeclarationsForImportOptions} {cursorSymbol, documentSource, documentPath} * @returns {(Promise)} - * + * * @memberof ImportResolveExtension */ private async getDeclarationsForImport( { cursorSymbol, documentSource, documentPath }: DeclarationsForImportOptions, ): Promise { this.logger.info(`Calculate possible imports for document with filter "${cursorSymbol}"`); + if (!window.activeTextEditor) { + return []; + } + + const index = this.indices.getIndexForFile(window.activeTextEditor.document.uri); + const rootFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + + if (!index || !index.indexReady || !rootFolder) { + return []; + } const parsedSource = await this.parser.parseSource(documentSource); const activeDocumentDeclarations = parsedSource.declarations.map(o => o.name); const declarations = getDeclarationsFilteredByImports( - this.index.declarationInfos, + index.declarationInfos, documentPath, parsedSource.imports, - this.rootPath, + rootFolder.uri.fsPath, ).filter(o => o.declaration.name.startsWith(cursorSymbol)); return [ @@ -516,24 +306,34 @@ export class ImportResolveExtension extends BaseExtension { /** * Calculates the missing imports of a document. Parses the documents source and then * tries to resolve all possible declaration infos for the usages (used identifiers). - * + * * @private * @param {MissingDeclarationsForFileOptions} {documentSource, documentPath} * @returns {(Promise<(DeclarationInfo | ImportUserDecision)[]>)} - * + * * @memberof ImportResolveExtension */ private async getMissingDeclarationsForFile( { documentSource, documentPath }: MissingDeclarationsForFileOptions, ): Promise<(DeclarationInfo)[]> { - // TODO + if (!window.activeTextEditor) { + return []; + } + + const index = this.indices.getIndexForFile(window.activeTextEditor.document.uri); + const rootFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + + if (!index || !index.indexReady || !rootFolder) { + return []; + } + const parsedDocument = await this.parser.parseSource(documentSource); const missingDeclarations: (DeclarationInfo)[] = []; const declarations = getDeclarationsFilteredByImports( - this.index.declarationInfos, + index.declarationInfos, documentPath, parsedDocument.imports, - this.rootPath, + rootFolder.uri.fsPath, ); for (const usage of parsedDocument.nonLocalUsages) { @@ -553,7 +353,7 @@ export class ImportResolveExtension extends BaseExtension { /** * Registers the commands for this extension. - * + * * @private * @memberof ImportResolveExtension */ @@ -576,7 +376,7 @@ export class ImportResolveExtension extends BaseExtension { commands.registerTextEditorCommand('typescriptHero.resolve.organizeImports', () => this.organizeImports()), ); this.context.subscriptions.push( - commands.registerCommand('typescriptHero.resolve.rebuildCache', () => this.buildIndex()), + commands.registerCommand('typescriptHero.resolve.rebuildCache', () => this.indices.rebuildAll()), ); } } diff --git a/src/extension/extensions/OrganizeImportsOnSaveExtension.ts b/src/extension/extensions/OrganizeImportsOnSaveExtension.ts index bdf1a54..c13526f 100644 --- a/src/extension/extensions/OrganizeImportsOnSaveExtension.ts +++ b/src/extension/extensions/OrganizeImportsOnSaveExtension.ts @@ -1,7 +1,7 @@ import { inject, injectable } from 'inversify'; import { ExtensionContext, workspace } from 'vscode'; -import { ExtensionConfig } from '../../common/config'; +import { ConfigFactory } from '../../common/factories'; import { Logger, LoggerFactory } from '../../common/utilities'; import { iocSymbols } from '../IoCSymbols'; import { ImportManager } from '../managers'; @@ -27,7 +27,7 @@ export class OrganizeImportsOnSaveExtension extends BaseExtension { constructor( @inject(iocSymbols.extensionContext) context: ExtensionContext, @inject(iocSymbols.loggerFactory) loggerFactory: LoggerFactory, - @inject(iocSymbols.configuration) private config: ExtensionConfig, + @inject(iocSymbols.configuration) private config: ConfigFactory, ) { super(context); this.logger = loggerFactory('OrganizeImportsOnSaveExtension'); @@ -40,7 +40,8 @@ export class OrganizeImportsOnSaveExtension extends BaseExtension { */ public initialize(): void { this.context.subscriptions.push(workspace.onWillSaveTextDocument((event) => { - if (!this.config.resolver.organizeOnSave) { + const config = this.config(event.document.uri); + if (!config.resolver.organizeOnSave) { this.logger.info('Organize on save is deactivated through config.'); return; } diff --git a/src/extension/managers/ClassManager.ts b/src/extension/managers/ClassManager.ts index 741faf1..7b77859 100644 --- a/src/extension/managers/ClassManager.ts +++ b/src/extension/managers/ClassManager.ts @@ -11,7 +11,6 @@ import { } from 'typescript-parser'; import { Position, Range, TextDocument, TextEdit, workspace, WorkspaceEdit } from 'vscode'; -import { ExtensionConfig } from '../../common/config'; import { ClassNotFoundError, MethodDuplicated, @@ -29,7 +28,7 @@ type VisibleObject = { visibility?: DeclarationVisibility }; /** * Sortfunction for changeable objects. Does sort the objects by visibility. - * + * * @param {Changeable} o1 * @param {Changeable} o2 * @returns {number} @@ -54,7 +53,7 @@ function sortByVisibility(o1: Changeable, o2: Changeable(iocSymbols.generatorFactory)(); } - private static get config(): ExtensionConfig { - return Container.get(iocSymbols.configuration); - } - private ctor: Changeable; private properties: Changeable[] = []; private methods: Changeable[] = []; @@ -90,12 +85,12 @@ export class ClassManager implements ObjectManager { * Creates an instance of a ClassManager. * Does parse the document text first and returns a promise that * resolves to a ClassManager. - * + * * @static * @param {TextDocument} document The document that should be managed * @param {string} className The name of the class that should be managed * @returns {Promise} - * + * * @memberof ClassManager */ public static async create(document: TextDocument, className: string): Promise { @@ -112,10 +107,10 @@ export class ClassManager implements ObjectManager { /** * Checks if a property with the given name exists on the virtual class. - * + * * @param {string} name * @returns {boolean} - * + * * @memberof ClassManager */ public hasProperty(name: string): boolean { @@ -124,12 +119,12 @@ export class ClassManager implements ObjectManager { /** * Add a property to the virtual class. Creates a Changeable object with the .isNew flag set to true. - * + * * @param {(string | PropertyDeclaration)} nameOrDeclaration * @param {DeclarationVisibility} [visibility] * @param {string} [type] * @returns {this} - * + * * @memberof ClassManager */ public addProperty( @@ -158,10 +153,10 @@ export class ClassManager implements ObjectManager { /** * Remove (aka set isDeleted) a property from the virtual class. - * + * * @param {string} name * @returns {this} - * + * * @memberof ClassManager */ public removeProperty(name: string): this { @@ -181,10 +176,10 @@ export class ClassManager implements ObjectManager { /** * Checks if a method with the given name does exist on the virtual class. - * + * * @param {string} name * @returns {boolean} - * + * * @memberof ClassManager */ public hasMethod(name: string): boolean { @@ -193,13 +188,13 @@ export class ClassManager implements ObjectManager { /** * Add a method to the virtual class. - * + * * @param {(string | MethodDeclaration)} nameOrDeclaration * @param {DeclarationVisibility} [visibility] * @param {string} [type] * @param {ParameterDeclaration[]} [parameters] * @returns {this} - * + * * @memberof ClassManager */ public addMethod( @@ -230,10 +225,10 @@ export class ClassManager implements ObjectManager { /** * Removes a method from the virtual class. - * + * * @param {string} name * @returns {this} - * + * * @memberof ClassManager */ public removeMethod(name: string): this { @@ -257,9 +252,9 @@ export class ClassManager implements ObjectManager { * - Delete properties * - Update changed properties (still TODO) * - Insert new properties - * + * * @returns {Promise} - * + * * @memberof ClassManager */ public async commit(): Promise { @@ -276,11 +271,11 @@ export class ClassManager implements ObjectManager { /** * Determines if a propertydeclaration is injected by the constructor. * I.e. constructor(public foo: string)... - * + * * @private * @param {PropertyDeclaration} property * @returns {boolean} - * + * * @memberof ClassManager */ private isInConstructor(property: PropertyDeclaration): boolean { @@ -293,10 +288,10 @@ export class ClassManager implements ObjectManager { /** * Calculates TextEdits for properties. - * + * * @private * @returns {TextEdit[]} - * + * * @memberof ClassManager */ private calculatePropertyEdits(): TextEdit[] { @@ -338,10 +333,10 @@ export class ClassManager implements ObjectManager { /** * Calculates TextEdits for methods. - * + * * @private * @returns {TextEdit[]} - * + * * @memberof ClassManager */ private calculateMethodEdits(): TextEdit[] { diff --git a/src/extension/managers/ImportManager.ts b/src/extension/managers/ImportManager.ts index 2bc5f0c..9562ba7 100644 --- a/src/extension/managers/ImportManager.ts +++ b/src/extension/managers/ImportManager.ts @@ -17,6 +17,7 @@ import { import { InputBoxOptions, Range, TextDocument, TextEdit, window, workspace, WorkspaceEdit } from 'vscode'; import { ExtensionConfig } from '../../common/config'; +import { ConfigFactory } from '../../common/factories'; import { getAbsolutLibraryName, getDeclarationsFilteredByImports, @@ -55,23 +56,28 @@ export class ImportManager implements ObjectManager { return Container.get(iocSymbols.typescriptParser); } - private static get config(): ExtensionConfig { - return Container.get(iocSymbols.configuration); + private static get config(): ConfigFactory { + return Container.get(iocSymbols.configuration); } private static get generator(): TypescriptCodeGenerator { return Container.get<() => TypescriptCodeGenerator>(iocSymbols.generatorFactory)(); } - private static get rootPath(): string { - return Container.get(iocSymbols.rootPath); - } - private importGroups: ImportGroup[]; private imports: Import[] = []; private userImportDecisions: { [usage: string]: DeclarationInfo[] }[] = []; private organize: boolean; + private get config(): ExtensionConfig { + return ImportManager.config(this.document.uri); + } + + private get rootPath(): string | undefined { + const rootFolder = workspace.getWorkspaceFolder(this.document.uri); + return rootFolder ? rootFolder.uri.fsPath : undefined; + } + /** * Document resource for this controller. Contains the parsed document. * @@ -113,7 +119,7 @@ export class ImportManager implements ObjectManager { */ public reset(): void { this.imports = this._parsedDocument.imports.map(o => o.clone()); - this.importGroups = ImportManager.config.resolver.importGroups; + this.importGroups = this.config.resolver.importGroups; this.addImportsToGroups(this.imports); } @@ -133,7 +139,7 @@ export class ImportManager implements ObjectManager { o => declarationInfo.from === getAbsolutLibraryName( o.libraryName, this.document.fileName, - ImportManager.rootPath, + this.rootPath, ) && o instanceof NamedImport, ) as NamedImport; @@ -149,7 +155,7 @@ export class ImportManager implements ObjectManager { let imp: Import = new NamedImport(getRelativeLibraryName( declarationInfo.from, this.document.fileName, - ImportManager.rootPath, + this.rootPath, )); if (declarationInfo.declaration instanceof ModuleDeclaration) { @@ -183,7 +189,7 @@ export class ImportManager implements ObjectManager { index.declarationInfos, this.document.fileName, this.imports, - ImportManager.rootPath, + this.rootPath, ); for (const usage of this._parsedDocument.nonLocalUsages) { @@ -213,11 +219,11 @@ export class ImportManager implements ObjectManager { this.organize = true; let keep: Import[] = []; - if (ImportManager.config.resolver.disableImportRemovalOnOrganize) { + if (this.config.resolver.disableImportRemovalOnOrganize) { keep = this.imports; } else { for (const actImport of this.imports) { - if (ImportManager.config.resolver.ignoreImportsForOrganize.indexOf(actImport.libraryName) >= 0) { + if (this.config.resolver.ignoreImportsForOrganize.indexOf(actImport.libraryName) >= 0) { keep.push(actImport); continue; } @@ -241,7 +247,7 @@ export class ImportManager implements ObjectManager { } } - if (!ImportManager.config.resolver.disableImportSorting) { + if (!this.config.resolver.disableImportSorting) { keep = [ ...keep.filter(o => o instanceof StringImport).sort(importSort), ...keep.filter(o => !(o instanceof StringImport)).sort(importSort), @@ -432,7 +438,7 @@ export class ImportManager implements ObjectManager { const specifiers = getSpecifiers(); if ( specifiers.filter(o => o === imp.defaultAlias).length > 1 && - ImportManager.config.resolver.promptForSpecifiers + this.config.resolver.promptForSpecifiers ) { imp.defaultAlias = await this.getDefaultIdentifier(imp.defaultAlias); } @@ -442,7 +448,7 @@ export class ImportManager implements ObjectManager { const specifiers = getSpecifiers(); if ( specifiers.filter(o => o === (spec.alias || spec.specifier)).length > 1 && - ImportManager.config.resolver.promptForSpecifiers + this.config.resolver.promptForSpecifiers ) { spec.alias = await this.getSpecifierAlias(spec.alias || spec.specifier); } diff --git a/src/extension/utilities/DeclarationIndexMapper.ts b/src/extension/utilities/DeclarationIndexMapper.ts new file mode 100644 index 0000000..122f784 --- /dev/null +++ b/src/extension/utilities/DeclarationIndexMapper.ts @@ -0,0 +1,174 @@ +import { inject, injectable, postConstruct } from 'inversify'; +import { DeclarationIndex, FileChanges, TypescriptParser } from 'typescript-parser'; +import { + Event, + EventEmitter, + ExtensionContext, + FileSystemWatcher, + RelativePattern, + Uri, + workspace, + WorkspaceFolder, + WorkspaceFoldersChangeEvent, +} from 'vscode'; + +import { ConfigFactory } from '../../common/factories'; +import { findFiles } from '../../common/helpers'; +import { Logger, LoggerFactory } from '../../common/utilities'; +import { iocSymbols } from '../../extension/IoCSymbols'; + +interface WorkspaceIndex { + index: DeclarationIndex; + folder: WorkspaceFolder; + watcher: FileSystemWatcher; +} + +// TODO move did change configuration to all indices +// TODO: update index on change of configs + +@injectable() +export class DeclarationIndexMapper { + public readonly onStartIndexing: Event; + public readonly onFinishIndexing: Event; + public readonly onIndexingError: Event<{ index: WorkspaceIndex, error: Error }>; + + private _onStartIndexing: EventEmitter; + private _onFinishIndexing: EventEmitter; + private _onIndexingError: EventEmitter<{ index: WorkspaceIndex, error: Error }>; + + private logger: Logger; + private indizes: { [uri: string]: WorkspaceIndex } = {}; + + constructor( + @inject(iocSymbols.loggerFactory) loggerFactory: LoggerFactory, + @inject(iocSymbols.extensionContext) private context: ExtensionContext, + @inject(iocSymbols.typescriptParser) private parser: TypescriptParser, + @inject(iocSymbols.configuration) private config: ConfigFactory, + ) { + this._onFinishIndexing = new EventEmitter(); + this._onStartIndexing = new EventEmitter(); + this._onIndexingError = new EventEmitter(); + + this.onFinishIndexing = this._onFinishIndexing.event; + this.onStartIndexing = this._onStartIndexing.event; + this.onIndexingError = this._onIndexingError.event; + + this.context.subscriptions.push(this._onFinishIndexing); + this.context.subscriptions.push(this._onIndexingError); + this.context.subscriptions.push(this._onStartIndexing); + + this.logger = loggerFactory('DeclarationIndexMapper'); + } + + @postConstruct() + public initialize(): void { + this.context.subscriptions.push(workspace.onDidChangeWorkspaceFolders(e => this.workspaceChanged(e))); + this.logger.info( + `Fired up index mapper, got ${(workspace.workspaceFolders || []).length} workspaces.`, + workspace.workspaceFolders, + ); + for (const folder of (workspace.workspaceFolders || []).filter(workspace => workspace.uri.scheme === 'file')) { + this.initializeIndex(folder); + } + this.logger.info('Initialized'); + } + + public rebuildAll(): void { + for (const index of Object.values(this.indizes)) { + index.watcher.dispose(); + index.index.reset(); + } + this.indizes = {}; + for (const folder of (workspace.workspaceFolders || []).filter(workspace => workspace.uri.scheme === 'file')) { + this.initializeIndex(folder); + } + } + + public getIndexForFile(fileUri: Uri): DeclarationIndex | undefined { + const workspaceFolder = workspace.getWorkspaceFolder(fileUri); + if (!workspaceFolder || !this.indizes[workspaceFolder.uri.fsPath]) { + return; + } + + return this.indizes[workspaceFolder.uri.fsPath].index; + } + + private workspaceChanged(event: WorkspaceFoldersChangeEvent): void { + this.logger.info('Workspaces changed.', event); + for (const add of event.added) { + if (this.indizes[add.uri.fsPath]) { + this.logger.warning(`The workspace with the path ${add.uri.fsPath} already exists. Skipping.`); + continue; + } + this.initializeIndex(add); + } + + for (const remove of event.removed) { + this.indizes[remove.uri.fsPath].index.reset(); + this.indizes[remove.uri.fsPath].watcher.dispose(); + delete this.indizes[remove.uri.fsPath]; + } + } + + private async initializeIndex(folder: WorkspaceFolder): Promise { + const index = new DeclarationIndex(this.parser, folder.uri.fsPath); + const config = this.config(folder.uri); + const files = await findFiles(config, folder); + const watcher = workspace.createFileSystemWatcher( + new RelativePattern( + folder, + `{${config.resolver.resolverModeFileGlobs.join(',')},**/package.json,**/typings.json}`, + ), + ); + const workspaceIndex = { + index, + folder, + watcher, + }; + + this._onStartIndexing.fire(workspaceIndex); + + let timeout: NodeJS.Timer | undefined; + let events: FileChanges | undefined; + + const fileWatcherEvent = (event: string, uri: Uri) => { + if (timeout) { + clearTimeout(timeout); + } + if (!events) { + events = { + created: [], + updated: [], + deleted: [], + }; + } + events[event].push(uri.fsPath); + + timeout = setTimeout( + async () => { + if (events) { + this.logger.info(`Refreshing index for workspace ${folder.name}.`); + await index.reindexForChanges(events); + this.logger.info(`Finished indexing for workspace ${folder.name}.`); + events = undefined; + } + }, + 500, + ); + }; + + watcher.onDidCreate(uri => fileWatcherEvent('created', uri)); + watcher.onDidChange(uri => fileWatcherEvent('updated', uri)); + watcher.onDidDelete(uri => fileWatcherEvent('deleted', uri)); + + try { + await index.buildIndex(files); + this.indizes[folder.uri.fsPath] = workspaceIndex; + this.logger.info(`Finished building index for workspace "${folder.name}".`); + this._onFinishIndexing.fire(workspaceIndex); + } catch (error) { + this.logger.error(`Error during build of index for workspace "${folder.name}"`, error); + this._onIndexingError.fire({ error, index: workspaceIndex }); + } + } +} diff --git a/src/extension/utilities/VscodeLogger.ts b/src/extension/utilities/VscodeLogger.ts index a804538..907d96a 100644 --- a/src/extension/utilities/VscodeLogger.ts +++ b/src/extension/utilities/VscodeLogger.ts @@ -5,7 +5,7 @@ import { ExtensionContext, OutputChannel, window } from 'vscode'; /** * Central logger instance of the extension. - * + * * @export * @class VscodeLogger */ @@ -21,10 +21,10 @@ export class VscodeLogger implements Logger { /** * Logs an error message. Provided data is logged out after the message. - * + * * @param {string} message * @param {*} [data] - * + * * @memberof VscodeLogger */ public error(message: string, data?: any): void { @@ -38,10 +38,10 @@ export class VscodeLogger implements Logger { /** * Logs a warning message. Provided data is logged out after the message. - * + * * @param {string} message * @param {*} [data] - * + * * @memberof VscodeLogger */ public warning(message: string, data?: any): void { @@ -55,10 +55,10 @@ export class VscodeLogger implements Logger { /** * Logs an info message. Provided data is logged out after the message. - * + * * @param {string} message * @param {*} [data] - * + * * @memberof VscodeLogger */ public info(message: string, data?: any): void { @@ -73,13 +73,13 @@ export class VscodeLogger implements Logger { /** * Internal method to actually do the logging. Checks if the output should be done and logs * the data into the output channel and the console (if debugging). - * + * * @private * @param {LogLevel} level * @param {string} severity * @param {string} message * @param {*} [data] - * + * * @memberof VscodeLogger */ private log(level: LogLevel, severity: string, message: string, data?: any): void { @@ -101,10 +101,10 @@ export class VscodeLogger implements Logger { /** * Returns a propper formatted date for logging. - * + * * @private * @returns {string} - * + * * @memberof Logger */ private getDate(): string { @@ -127,10 +127,10 @@ export class VscodeLogger implements Logger { /** * Maps the configuration string to a propper enum value of LogLevel. - * + * * @private * @returns {LogLevel} - * + * * @memberof VscodeLogger */ private getLogLevel(): LogLevel { diff --git a/test/_workspace/.vscode/settings.json b/test/_workspace/.vscode/settings.json index e1da5dd..c5f10cc 100755 --- a/test/_workspace/.vscode/settings.json +++ b/test/_workspace/.vscode/settings.json @@ -14,5 +14,7 @@ "typescriptHero.codeOutline.enabled": true, "typescriptHero.resolver.organizeOnSave": false, "typescriptHero.resolver.promptForSpecifiers": true, - "editor.formatOnSave": true + "editor.formatOnSave": true, + "typescriptHero.codeCompletion.completionSortMode": "default", + "typescriptHero.resolver.disableImportRemovalOnOrganize": false } diff --git a/test/_workspace_2/file1.ts b/test/_workspace_2/file1.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/_workspace_2/file2.ts b/test/_workspace_2/file2.ts new file mode 100644 index 0000000..5ef1a5e --- /dev/null +++ b/test/_workspace_2/file2.ts @@ -0,0 +1,3 @@ +export class ClassInFile2 { } + +export default class DefaultClassInFile2 { } diff --git a/test/multi-root-workspace-tests/MultiRootIndices.test.ts b/test/multi-root-workspace-tests/MultiRootIndices.test.ts new file mode 100644 index 0000000..5d02a97 --- /dev/null +++ b/test/multi-root-workspace-tests/MultiRootIndices.test.ts @@ -0,0 +1,6 @@ + +describe('Multi root indices', () => { + + it('should be tested; will be done as soon as the feature is default enabled.'); + +}); diff --git a/test/index.ts b/test/multi-root-workspace-tests/index.ts similarity index 92% rename from test/index.ts rename to test/multi-root-workspace-tests/index.ts index 31f24fd..9956ef9 100644 --- a/test/index.ts +++ b/test/multi-root-workspace-tests/index.ts @@ -2,8 +2,8 @@ import 'reflect-metadata'; import { ExtensionContext, Memento } from 'vscode'; -import { Container } from '../src/extension/IoC'; -import { iocSymbols } from '../src/extension/IoCSymbols'; +import { Container } from '../../src/extension/IoC'; +import { iocSymbols } from '../../src/extension/IoCSymbols'; // tslint:disable @@ -19,7 +19,6 @@ import { iocSymbols } from '../src/extension/IoCSymbols'; // to report the results back to the caller. When the tests are finished, return // a possible error to the callback or null if none. - class ContextMock implements ExtensionContext { subscriptions: { dispose(): any }[] = []; workspaceState: Memento; diff --git a/test/multi-root.code-workspace b/test/multi-root.code-workspace new file mode 100644 index 0000000..7cc4784 --- /dev/null +++ b/test/multi-root.code-workspace @@ -0,0 +1,20 @@ +{ + "folders": [ + { + "path": "_workspace" + }, + { + "path": "_workspace_2" + }, + { + "path": "_workspace/server" + } + ], + "settings": { + "typescriptHero.verbosity": "Warnings", + "typescriptHero.codeOutline.enabled": true, + "typescriptHero.resolver.organizeOnSave": false, + "typescriptHero.resolver.promptForSpecifiers": true, + "editor.formatOnSave": true + } +} diff --git a/test/common/helpers/ImportHelpers.test.ts b/test/single-workspace-tests/common/helpers/ImportHelpers.test.ts similarity index 96% rename from test/common/helpers/ImportHelpers.test.ts rename to test/single-workspace-tests/common/helpers/ImportHelpers.test.ts index 9adafd5..781d69a 100644 --- a/test/common/helpers/ImportHelpers.test.ts +++ b/test/single-workspace-tests/common/helpers/ImportHelpers.test.ts @@ -1,6 +1,6 @@ import * as chai from 'chai'; -import { getImportInsertPosition } from '../../../src/common/helpers'; +import { getImportInsertPosition } from '../../../../src/common/helpers'; chai.should(); diff --git a/test/extension/extensions/CodeActionExtension.test.ts b/test/single-workspace-tests/extension/extensions/CodeActionExtension.test.ts similarity index 90% rename from test/extension/extensions/CodeActionExtension.test.ts rename to test/single-workspace-tests/extension/extensions/CodeActionExtension.test.ts index f9d0acc..d5f6b59 100644 --- a/test/extension/extensions/CodeActionExtension.test.ts +++ b/test/single-workspace-tests/extension/extensions/CodeActionExtension.test.ts @@ -1,20 +1,24 @@ import * as chai from 'chai'; import { join } from 'path'; import * as sinon from 'sinon'; +import sinonChai = require('sinon-chai'); import { DeclarationIndex, TypescriptParser } from 'typescript-parser'; import { ExtensionContext, Position, Range, TextDocument, window, workspace } from 'vscode'; -import { LoggerFactory } from '../../../src/common/utilities'; -import { AddImportCodeAction, CodeAction, ImplementPolymorphElements } from '../../../src/extension/code-actions/CodeAction'; +import { ConfigFactory } from '../../../../src/common/factories'; +import { LoggerFactory } from '../../../../src/common/utilities'; +import { AddImportCodeAction, CodeAction, ImplementPolymorphElements } from '../../../../src/extension/code-actions'; import { MissingImplementationInClassCreator, -} from '../../../src/extension/code-actions/MissingImplementationInClassCreator'; -import { MissingImportCreator } from '../../../src/extension/code-actions/MissingImportCreator'; -import { CodeActionExtension } from '../../../src/extension/extensions/CodeActionExtension'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; +} from '../../../../src/extension/code-actions/MissingImplementationInClassCreator'; +import { MissingImportCreator } from '../../../../src/extension/code-actions/MissingImportCreator'; +import { CodeActionExtension } from '../../../../src/extension/extensions/CodeActionExtension'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; +import { DeclarationIndexMapper } from '../../../../src/extension/utilities/DeclarationIndexMapper'; chai.should(); +chai.use(sinonChai); class SpyCodeAction implements CodeAction { constructor(private spy: sinon.SinonSpy, private result: boolean) { } @@ -25,18 +29,19 @@ class SpyCodeAction implements CodeAction { } } -const rootPath = Container.get(iocSymbols.rootPath); - describe('CodeActionExtension', () => { + const rootPath = workspace.workspaceFolders![0].uri.fsPath; let extension: any; before(async () => { const ctx = Container.get(iocSymbols.extensionContext); const logger = Container.get(iocSymbols.loggerFactory); const parser = Container.get(iocSymbols.typescriptParser); + const config = Container.get(iocSymbols.configuration); + const fakeMapper = new DeclarationIndexMapper(logger, ctx, parser, config); - const index = Container.get(iocSymbols.declarationIndex); + const index = new DeclarationIndex(parser, rootPath); await index.buildIndex( [ join( @@ -58,12 +63,14 @@ describe('CodeActionExtension', () => { ], ); + fakeMapper.getIndexForFile = sinon.spy(() => index); + const creators = [ - new MissingImportCreator(index as any), - new MissingImplementationInClassCreator(parser, index as any, rootPath), + new MissingImportCreator(fakeMapper), + new MissingImplementationInClassCreator(parser, fakeMapper), ]; - extension = new CodeActionExtension(ctx, logger, creators, index as any); + extension = new CodeActionExtension(ctx, logger, creators, fakeMapper); }); describe('executeCodeAction', () => { @@ -187,7 +194,7 @@ describe('CodeActionExtension', () => { diagnostics: [ { message: - `non-abstract class 'Foobar' implement inherited from class 'CodeFixImplementAbstract'.`, + `non-abstract class 'Foobar' implement inherited from class 'CodeFixImplementAbstract'.`, }, ], }, @@ -254,7 +261,7 @@ describe('CodeActionExtension', () => { diagnostics: [ { message: `non-abstract class 'Foobar' ` + - `implement inherited from class 'GenericAbstractClass'.`, + `implement inherited from class 'GenericAbstractClass'.`, }, ], }, @@ -310,7 +317,7 @@ describe('CodeActionExtension', () => { diagnostics: [ { message: `non-abstract class 'AbstractImplement' ` + - `implement inherited from class 'CodeFixImplementAbstract'.`, + `implement inherited from class 'CodeFixImplementAbstract'.`, }, ], }, @@ -358,8 +365,8 @@ describe('CodeActionExtension', () => { diagnostics: [ { message: - `non-abstract class 'InternalAbstractImplement' ` + - `implement inherited from class 'InternalAbstract'.`, + `non-abstract class 'InternalAbstractImplement' ` + + `implement inherited from class 'InternalAbstract'.`, }, ], }, @@ -380,8 +387,8 @@ describe('CodeActionExtension', () => { { diagnostics: [ { - message: - `class 'ImplementGenericInterface' incorrectly implements 'GenericInterface'.`, + message: `class 'ImplementGenericInterface' incorrectly ` + + `implements 'GenericInterface'.`, }, ], }, @@ -405,8 +412,8 @@ describe('CodeActionExtension', () => { diagnostics: [ { message: - `non-abstract class 'ImplementGenericAbstract' ` + - `implement inherited from class 'GenericAbstractClass'.`, + `non-abstract class 'ImplementGenericAbstract' ` + + `implement inherited from class 'GenericAbstractClass'.`, }, ], }, @@ -462,7 +469,7 @@ describe('CodeActionExtension', () => { cmds.should.have.lengthOf(2); const action = cmds[0]; - action.title.should.equal('Import "Class1" from "/server/indices".'); + action.title.should.equal('Import "Class1" from "/server/indices/MyClass".'); action.arguments[0].should.be.an.instanceof(AddImportCodeAction); }); @@ -487,7 +494,7 @@ describe('CodeActionExtension', () => { cmds.should.have.lengthOf(3); let action = cmds[0]; - action.title.should.equal('Import "FancierLibraryClass" from "/server/indices".'); + action.title.should.equal('Import "FancierLibraryClass" from "/server/indices/MyClass".'); action.arguments[0].should.be.an.instanceof(AddImportCodeAction); action = cmds[1]; diff --git a/test/extension/extensions/CodeCompletionExtension.test.ts b/test/single-workspace-tests/extension/extensions/CodeCompletionExtension.test.ts similarity index 72% rename from test/extension/extensions/CodeCompletionExtension.test.ts rename to test/single-workspace-tests/extension/extensions/CodeCompletionExtension.test.ts index 8beca1a..a42c6ed 100644 --- a/test/extension/extensions/CodeCompletionExtension.test.ts +++ b/test/single-workspace-tests/extension/extensions/CodeCompletionExtension.test.ts @@ -1,24 +1,25 @@ import * as chai from 'chai'; import { join } from 'path'; +import * as sinon from 'sinon'; import { DeclarationIndex, TypescriptParser } from 'typescript-parser'; import * as vscode from 'vscode'; -import { ExtensionConfig } from '../../../src/common/config'; -import { LoggerFactory } from '../../../src/common/utilities'; -import { CodeCompletionExtension } from '../../../src/extension/extensions/CodeCompletionExtension'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; +import { ConfigFactory } from '../../../../src/common/factories'; +import { LoggerFactory } from '../../../../src/common/utilities'; +import { CodeCompletionExtension } from '../../../../src/extension/extensions/CodeCompletionExtension'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; +import { DeclarationIndexMapper } from '../../../../src/extension/utilities/DeclarationIndexMapper'; +import { VscodeExtensionConfig } from '../../../../src/extension/config/VscodeExtensionConfig'; const should = chai.should(); -const rootPath = Container.get(iocSymbols.rootPath); - describe('CodeCompletionExtension', () => { + const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; const token = new vscode.CancellationTokenSource().token; let document: vscode.TextDocument; let extension: CodeCompletionExtension; - let config: ExtensionConfig; before(async () => { const file = join( @@ -31,9 +32,10 @@ describe('CodeCompletionExtension', () => { const ctx = Container.get(iocSymbols.extensionContext); const logger = Container.get(iocSymbols.loggerFactory); const parser = Container.get(iocSymbols.typescriptParser); - const index = Container.get(iocSymbols.declarationIndex); - config = Container.get(iocSymbols.configuration); + const config = Container.get(iocSymbols.configuration); + const fakeMapper = new DeclarationIndexMapper(logger, ctx, parser, config); + const index = new DeclarationIndex(parser, rootPath); await index.buildIndex( [ join( @@ -47,7 +49,9 @@ describe('CodeCompletionExtension', () => { ], ); - extension = new CodeCompletionExtension(ctx, logger, parser, index as any, rootPath, config); + fakeMapper.getIndexForFile = sinon.spy(() => index); + + extension = new CodeCompletionExtension(ctx, logger, parser, fakeMapper, config); }); it('shoud resolve to null if typing in a string', async () => { @@ -85,12 +89,21 @@ describe('CodeCompletionExtension', () => { }); it('should use custom sort order when config.completionSortMode is bottom', async () => { - Object.defineProperty(config, 'completionSortMode', { value: 'bottom', writable: true }); - const result = await extension.provideCompletionItems(document, new vscode.Position(6, 5), token); - should.exist(result![0].sortText); - result![0].sortText!.should.equal('9999-MyClass'); - - Object.defineProperty(config, 'completionSortMode', { value: 'default' }); + const orig = (extension as any).config; + const config = new VscodeExtensionConfig(); + (config as any).codeCompletionConfig = { + completionSortMode: 'bottom', + }; + + (extension as any).config = sinon.spy(() => config); + + try { + const result = await extension.provideCompletionItems(document, new vscode.Position(6, 5), token); + should.exist(result![0].sortText); + result![0].sortText!.should.equal('9999-MyClass'); + } finally { + (extension as any).config = orig; + } }); it('shoud add an insert command text edit if import would be new', async () => { diff --git a/test/extension/extensions/DocumentSymbolStructureExtension.test.ts b/test/single-workspace-tests/extension/extensions/DocumentSymbolStructureExtension.test.ts similarity index 74% rename from test/extension/extensions/DocumentSymbolStructureExtension.test.ts rename to test/single-workspace-tests/extension/extensions/DocumentSymbolStructureExtension.test.ts index 817bd95..9930532 100644 --- a/test/extension/extensions/DocumentSymbolStructureExtension.test.ts +++ b/test/single-workspace-tests/extension/extensions/DocumentSymbolStructureExtension.test.ts @@ -2,27 +2,29 @@ import { join } from 'path'; import { TypescriptParser } from 'typescript-parser'; import * as vscode from 'vscode'; -import { ExtensionConfig } from '../../../src/common/config'; -import { LoggerFactory } from '../../../src/common/utilities'; -import { DocumentSymbolStructureExtension } from '../../../src/extension/extensions/DocumentSymbolStructureExtension'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; -import { BaseStructureTreeItem } from '../../../src/extension/provider-items/document-structure/BaseStructureTreeItem'; +import { ConfigFactory } from '../../../../src/common/factories'; +import { LoggerFactory } from '../../../../src/common/utilities'; +import { DocumentSymbolStructureExtension } from '../../../../src/extension/extensions/DocumentSymbolStructureExtension'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; +import { BaseStructureTreeItem } from '../../../../src/extension/provider-items/document-structure/BaseStructureTreeItem'; import { DeclarationStructureTreeItem, -} from '../../../src/extension/provider-items/document-structure/DeclarationStructureTreeItem'; +} from '../../../../src/extension/provider-items/document-structure/DeclarationStructureTreeItem'; import { DisabledStructureTreeItem, -} from '../../../src/extension/provider-items/document-structure/DisabledStructureTreeItem'; -import { ImportsStructureTreeItem } from '../../../src/extension/provider-items/document-structure/ImportsStructureTreeItem'; +} from '../../../../src/extension/provider-items/document-structure/DisabledStructureTreeItem'; +import { + ImportsStructureTreeItem, +} from '../../../../src/extension/provider-items/document-structure/ImportsStructureTreeItem'; import { NotParseableStructureTreeItem, -} from '../../../src/extension/provider-items/document-structure/NotParseableStructureTreeItem'; +} from '../../../../src/extension/provider-items/document-structure/NotParseableStructureTreeItem'; -const rootPath = Container.get(iocSymbols.rootPath); describe('DocumentSymbolStructureExtension', () => { + const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; let extension: DocumentSymbolStructureExtension; const file = join( rootPath, @@ -33,7 +35,7 @@ describe('DocumentSymbolStructureExtension', () => { const ctx = Container.get(iocSymbols.extensionContext); const logger = Container.get(iocSymbols.loggerFactory); const parser = Container.get(iocSymbols.typescriptParser); - const config = Container.get(iocSymbols.configuration); + const config = Container.get(iocSymbols.configuration); extension = new DocumentSymbolStructureExtension(ctx, logger, config, parser); }); @@ -55,7 +57,6 @@ describe('DocumentSymbolStructureExtension', () => { }); it('should return a "file not parsable" if it is no ts file', async () => { - const rootPath = Container.get(iocSymbols.rootPath); const file = join( rootPath, 'extension/extensions/documentSymbolStructureExtension/notParsable.txt', diff --git a/test/extension/extensions/ImportResolveExtension.test.ts b/test/single-workspace-tests/extension/extensions/ImportResolveExtension.test.ts similarity index 90% rename from test/extension/extensions/ImportResolveExtension.test.ts rename to test/single-workspace-tests/extension/extensions/ImportResolveExtension.test.ts index d6708c8..906ec3e 100644 --- a/test/extension/extensions/ImportResolveExtension.test.ts +++ b/test/single-workspace-tests/extension/extensions/ImportResolveExtension.test.ts @@ -1,20 +1,22 @@ import * as chai from 'chai'; import { join } from 'path'; +import * as sinon from 'sinon'; import { DeclarationIndex, TypescriptParser } from 'typescript-parser'; import * as vscode from 'vscode'; -import { ExtensionConfig } from '../../../src/common/config'; -import { LoggerFactory } from '../../../src/common/utilities'; -import { ImportResolveExtension } from '../../../src/extension/extensions/ImportResolveExtension'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; +import { ConfigFactory } from '../../../../src/common/factories'; +import { LoggerFactory } from '../../../../src/common/utilities'; +import { ImportResolveExtension } from '../../../../src/extension/extensions/ImportResolveExtension'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; +import { DeclarationIndexMapper } from '../../../../src/extension/utilities/DeclarationIndexMapper'; chai.should(); -const rootPath = Container.get(iocSymbols.rootPath); describe('TypeScript Mode: ImportResolveExtension', () => { + const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; let extension: any; before(async () => { @@ -28,10 +30,11 @@ describe('TypeScript Mode: ImportResolveExtension', () => { const ctx = Container.get(iocSymbols.extensionContext); const logger = Container.get(iocSymbols.loggerFactory); - const config = Container.get(iocSymbols.configuration); + const config = Container.get(iocSymbols.configuration); const parser = Container.get(iocSymbols.typescriptParser); + const fakeMapper = new DeclarationIndexMapper(logger, ctx, parser, config); - const index = Container.get(iocSymbols.declarationIndex); + const index = new DeclarationIndex(parser, rootPath); await index.buildIndex( [ join( @@ -53,7 +56,9 @@ describe('TypeScript Mode: ImportResolveExtension', () => { ], ); - extension = new ImportResolveExtension(ctx, logger, config, parser, index, rootPath); + fakeMapper.getIndexForFile = sinon.spy(() => index); + + extension = new ImportResolveExtension(ctx, logger, parser, fakeMapper); }); describe('addImportToDocument', () => { @@ -184,7 +189,7 @@ describe('TypeScript Mode: ImportResolveExtension', () => { }); }); - + describe('organizeImports with exports', () => { const file = join(rootPath, 'extension/extensions/importResolveExtension/organizeImportsWithExports.ts'); @@ -223,6 +228,7 @@ describe('TypeScript Mode: ImportResolveExtension', () => { describe('JavaScript Mode: ImportResolveExtension', () => { + const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; let extension: any; before(async () => { @@ -236,10 +242,11 @@ describe('JavaScript Mode: ImportResolveExtension', () => { const ctx = Container.get(iocSymbols.extensionContext); const logger = Container.get(iocSymbols.loggerFactory); - const config = Container.get(iocSymbols.configuration); + const config = Container.get(iocSymbols.configuration); const parser = Container.get(iocSymbols.typescriptParser); + const fakeMapper = new DeclarationIndexMapper(logger, ctx, parser, config); - const index = Container.get(iocSymbols.declarationIndex); + const index = new DeclarationIndex(parser, rootPath); await index.buildIndex( [ join( @@ -257,7 +264,9 @@ describe('JavaScript Mode: ImportResolveExtension', () => { ], ); - extension = new ImportResolveExtension(ctx, logger, config, parser, index, rootPath); + fakeMapper.getIndexForFile = sinon.spy(() => index); + + extension = new ImportResolveExtension(ctx, logger, parser, fakeMapper); }); describe('addImportToDocument', () => { @@ -318,6 +327,7 @@ describe('JavaScript Mode: ImportResolveExtension', () => { describe('Mixed Mode: ImportResolveExtension', () => { + const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; let extension: any; before(async () => { @@ -331,10 +341,11 @@ describe('Mixed Mode: ImportResolveExtension', () => { const ctx = Container.get(iocSymbols.extensionContext); const logger = Container.get(iocSymbols.loggerFactory); - const config = Container.get(iocSymbols.configuration); + const config = Container.get(iocSymbols.configuration); const parser = Container.get(iocSymbols.typescriptParser); + const fakeMapper = new DeclarationIndexMapper(logger, ctx, parser, config); - const index = Container.get(iocSymbols.declarationIndex); + const index = new DeclarationIndex(parser, rootPath); await index.buildIndex( [ join( @@ -356,7 +367,9 @@ describe('Mixed Mode: ImportResolveExtension', () => { ], ); - extension = new ImportResolveExtension(ctx, logger, config, parser, index, rootPath); + fakeMapper.getIndexForFile = sinon.spy(() => index); + + extension = new ImportResolveExtension(ctx, logger, parser, fakeMapper); }); describe('addImportToDocument in .js file', () => { @@ -399,7 +412,7 @@ describe('Mixed Mode: ImportResolveExtension', () => { await extension.addImportToDocument(items[0]); document.getText().should.equal(`import { JSFoobar } from './jsfile';\n`); }); - + it('shoud write a named import correctly (TS)', async () => { const items = await extension.getDeclarationsForImport({ cursorSymbol: 'AddImportSameDirectory', @@ -411,7 +424,7 @@ describe('Mixed Mode: ImportResolveExtension', () => { }); }); - + describe('addImportToDocument in .ts file', () => { const file = join( rootPath, @@ -452,7 +465,7 @@ describe('Mixed Mode: ImportResolveExtension', () => { await extension.addImportToDocument(items[0]); document.getText().should.equal(`import { JSFoobar } from './jsfile';\n`); }); - + it('shoud write a named import correctly (TS)', async () => { const items = await extension.getDeclarationsForImport({ cursorSymbol: 'AddImportSameDirectory', diff --git a/test/extension/extensions/OrganizeImportsOnSaveExtension.test.ts b/test/single-workspace-tests/extension/extensions/OrganizeImportsOnSaveExtension.test.ts similarity index 91% rename from test/extension/extensions/OrganizeImportsOnSaveExtension.test.ts rename to test/single-workspace-tests/extension/extensions/OrganizeImportsOnSaveExtension.test.ts index 71d138b..3cda502 100644 --- a/test/extension/extensions/OrganizeImportsOnSaveExtension.test.ts +++ b/test/single-workspace-tests/extension/extensions/OrganizeImportsOnSaveExtension.test.ts @@ -3,16 +3,16 @@ import { join } from 'path'; import { DeclarationIndex } from 'typescript-parser'; import * as vscode from 'vscode'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; -import { ImportManager } from '../../../src/extension/managers'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; +import { ImportManager } from '../../../../src/extension/managers'; chai.should(); -const rootPath = Container.get(iocSymbols.rootPath); describe('OrganizeImportsOnSaveExtension', () => { + const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; let document: vscode.TextDocument; let index: DeclarationIndex; @@ -25,7 +25,7 @@ describe('OrganizeImportsOnSaveExtension', () => { await vscode.window.showTextDocument(document); - index = Container.get(iocSymbols.declarationIndex); + index = new DeclarationIndex(Container.get(iocSymbols.typescriptParser), rootPath); await index.buildIndex( [ join( diff --git a/test/extension/import-grouping/ImportGroupSettingParser.test.ts b/test/single-workspace-tests/extension/import-grouping/ImportGroupSettingParser.test.ts similarity index 97% rename from test/extension/import-grouping/ImportGroupSettingParser.test.ts rename to test/single-workspace-tests/extension/import-grouping/ImportGroupSettingParser.test.ts index f5148ee..2f981c3 100644 --- a/test/extension/import-grouping/ImportGroupSettingParser.test.ts +++ b/test/single-workspace-tests/extension/import-grouping/ImportGroupSettingParser.test.ts @@ -1,11 +1,12 @@ +import * as chai from 'chai'; + import { ImportGroupIdentifierInvalidError, ImportGroupKeyword, ImportGroupSettingParser, KeywordImportGroup, RegexImportGroup, -} from '../../../src/extension/import-grouping'; -import * as chai from 'chai'; +} from '../../../../src/extension/import-grouping'; chai.should(); diff --git a/test/extension/import-grouping/KeywordImportGroup.test.ts b/test/single-workspace-tests/extension/import-grouping/KeywordImportGroup.test.ts similarity index 92% rename from test/extension/import-grouping/KeywordImportGroup.test.ts rename to test/single-workspace-tests/extension/import-grouping/KeywordImportGroup.test.ts index b63f8d6..29c82f2 100644 --- a/test/extension/import-grouping/KeywordImportGroup.test.ts +++ b/test/single-workspace-tests/extension/import-grouping/KeywordImportGroup.test.ts @@ -1,27 +1,25 @@ import * as chai from 'chai'; import { join } from 'path'; import { File, TypescriptCodeGenerator, TypescriptParser } from 'typescript-parser'; +import { workspace } from 'vscode'; -import { ExtensionConfig } from '../../../src/common/config'; -import { TypescriptCodeGeneratorFactory } from '../../../src/common/factories'; -import { ImportGroupKeyword, KeywordImportGroup } from '../../../src/extension/import-grouping'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; +import { TypescriptCodeGeneratorFactory } from '../../../../src/common/factories'; +import { ImportGroupKeyword, KeywordImportGroup } from '../../../../src/extension/import-grouping'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; chai.should(); -const rootPath = Container.get(iocSymbols.rootPath); describe('KeywordImportGroup', () => { + const rootPath = workspace.workspaceFolders![0].uri.fsPath; let file: File; let importGroup: KeywordImportGroup; - let config: ExtensionConfig; let generator: TypescriptCodeGenerator; before(async () => { const parser = Container.get(iocSymbols.typescriptParser); - config = Container.get(iocSymbols.configuration); generator = Container.get(iocSymbols.generatorFactory)(); file = await parser.parseFile( join( diff --git a/test/extension/import-grouping/RegexImportGroup.test.ts b/test/single-workspace-tests/extension/import-grouping/RegexImportGroup.test.ts similarity index 86% rename from test/extension/import-grouping/RegexImportGroup.test.ts rename to test/single-workspace-tests/extension/import-grouping/RegexImportGroup.test.ts index 74b2ed1..49e88c5 100644 --- a/test/extension/import-grouping/RegexImportGroup.test.ts +++ b/test/single-workspace-tests/extension/import-grouping/RegexImportGroup.test.ts @@ -1,27 +1,24 @@ import * as chai from 'chai'; import { join } from 'path'; import { File, NamedImport, TypescriptCodeGenerator, TypescriptParser } from 'typescript-parser'; +import { workspace } from 'vscode'; -import { ExtensionConfig } from '../../../src/common/config'; -import { TypescriptCodeGeneratorFactory } from '../../../src/common/factories'; -import { RegexImportGroup } from '../../../src/extension/import-grouping'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; +import { TypescriptCodeGeneratorFactory } from '../../../../src/common/factories'; +import { RegexImportGroup } from '../../../../src/extension/import-grouping'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; chai.should(); -const rootPath = Container.get(iocSymbols.rootPath); - describe('RegexImportGroup', () => { + const rootPath = workspace.workspaceFolders![0].uri.fsPath; let file: File; let importGroup: RegexImportGroup; - let config: ExtensionConfig; let generator: TypescriptCodeGenerator; before(async () => { const parser = Container.get(iocSymbols.typescriptParser); - config = Container.get(iocSymbols.configuration); generator = Container.get(iocSymbols.generatorFactory)(); file = await parser.parseFile( join( diff --git a/test/extension/import-grouping/RemainImportGroup.test.ts b/test/single-workspace-tests/extension/import-grouping/RemainImportGroup.test.ts similarity index 81% rename from test/extension/import-grouping/RemainImportGroup.test.ts rename to test/single-workspace-tests/extension/import-grouping/RemainImportGroup.test.ts index 54209a0..647b9e9 100644 --- a/test/extension/import-grouping/RemainImportGroup.test.ts +++ b/test/single-workspace-tests/extension/import-grouping/RemainImportGroup.test.ts @@ -1,27 +1,24 @@ import * as chai from 'chai'; import { join } from 'path'; import { File, TypescriptCodeGenerator, TypescriptParser } from 'typescript-parser'; +import { workspace } from 'vscode'; -import { ExtensionConfig } from '../../../src/common/config'; -import { TypescriptCodeGeneratorFactory } from '../../../src/common/factories'; -import { RemainImportGroup } from '../../../src/extension/import-grouping'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; +import { TypescriptCodeGeneratorFactory } from '../../../../src/common/factories'; +import { RemainImportGroup } from '../../../../src/extension/import-grouping'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; chai.should(); -const rootPath = Container.get(iocSymbols.rootPath); - describe('RemainImportGroup', () => { + const rootPath = workspace.workspaceFolders![0].uri.fsPath; let file: File; let importGroup: RemainImportGroup; - let config: ExtensionConfig; let generator: TypescriptCodeGenerator; before(async () => { const parser = Container.get(iocSymbols.typescriptParser); - config = Container.get(iocSymbols.configuration); generator = Container.get(iocSymbols.generatorFactory)(); file = await parser.parseFile( join( diff --git a/test/extension/managers/ClassManager.test.ts b/test/single-workspace-tests/extension/managers/ClassManager.test.ts similarity index 96% rename from test/extension/managers/ClassManager.test.ts rename to test/single-workspace-tests/extension/managers/ClassManager.test.ts index c596860..3abddcd 100644 --- a/test/extension/managers/ClassManager.test.ts +++ b/test/single-workspace-tests/extension/managers/ClassManager.test.ts @@ -10,19 +10,18 @@ import { } from 'typescript-parser'; import { Position, Range, TextDocument, window, workspace } from 'vscode'; -import { findFiles } from '../../../src/extension/extensions/ImportResolveExtension'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; -import { ClassManager } from '../../../src/extension/managers/ClassManager'; -import { VscodeExtensionConfig } from '../../../src/extension/VscodeExtensionConfig'; +import { findFiles } from '../../../../src/common/helpers'; +import { VscodeExtensionConfig } from '../../../../src/extension/config/VscodeExtensionConfig'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; +import { ClassManager } from '../../../../src/extension/managers/ClassManager'; const should = chai.should(); chai.use(sinonChai); -const rootPath = Container.get(iocSymbols.rootPath); - describe('ClassManager', () => { + const rootPath = workspace.workspaceFolders![0].uri.fsPath; const file = join(rootPath, 'extension/managers/ClassManagerFile.ts'); let document: TextDocument; let documentText: string; @@ -30,10 +29,10 @@ describe('ClassManager', () => { let files: string[]; before(async () => { - const config = new VscodeExtensionConfig(); - files = await findFiles(config, rootPath); + const config = new VscodeExtensionConfig(workspace.workspaceFolders![0].uri); + files = await findFiles(config, workspace.workspaceFolders![0]); - index = Container.get(iocSymbols.declarationIndex); + index = new DeclarationIndex(Container.get(iocSymbols.typescriptParser), rootPath); await index.buildIndex(files); document = await workspace.openTextDocument(file); diff --git a/test/extension/managers/ImportManager.test.ts b/test/single-workspace-tests/extension/managers/ImportManager.test.ts similarity index 97% rename from test/extension/managers/ImportManager.test.ts rename to test/single-workspace-tests/extension/managers/ImportManager.test.ts index 04c1250..749c7f2 100644 --- a/test/extension/managers/ImportManager.test.ts +++ b/test/single-workspace-tests/extension/managers/ImportManager.test.ts @@ -5,11 +5,11 @@ import sinonChai = require('sinon-chai'); import { DeclarationIndex, File, NamedImport } from 'typescript-parser'; import { Position, Range, TextDocument, window, workspace } from 'vscode'; -import { findFiles } from '../../../src/extension/extensions/ImportResolveExtension'; -import { Container } from '../../../src/extension/IoC'; -import { iocSymbols } from '../../../src/extension/IoCSymbols'; -import { ImportManager } from '../../../src/extension/managers'; -import { VscodeExtensionConfig } from '../../../src/extension/VscodeExtensionConfig'; +import { findFiles } from '../../../../src/common/helpers'; +import { VscodeExtensionConfig } from '../../../../src/extension/config/VscodeExtensionConfig'; +import { Container } from '../../../../src/extension/IoC'; +import { iocSymbols } from '../../../../src/extension/IoCSymbols'; +import { ImportManager } from '../../../../src/extension/managers'; const should = chai.should(); chai.use(sinonChai); @@ -35,10 +35,9 @@ function restoreInputBox(stub: sinon.SinonStub): void { stub.restore(); } -const rootPath = Container.get(iocSymbols.rootPath); - describe('ImportManager', () => { + const rootPath = workspace.workspaceFolders![0].uri.fsPath; const file = join(rootPath, 'extension/managers/ImportManagerFile.ts'); let document: TextDocument; let documentText: string; @@ -46,10 +45,10 @@ describe('ImportManager', () => { let files: string[]; before(async () => { - const config = new VscodeExtensionConfig(); - files = await findFiles(config, rootPath); + const config = new VscodeExtensionConfig(workspace.workspaceFolders![0].uri); + files = await findFiles(config, workspace.workspaceFolders![0]); - index = index = Container.get(iocSymbols.declarationIndex); + index = new DeclarationIndex(Container.get(iocSymbols.typescriptParser), rootPath); await index.buildIndex(files); document = await workspace.openTextDocument(file); @@ -886,6 +885,7 @@ describe('ImportManager', () => { describe('ImportManager with .tsx files', () => { + const rootPath = workspace.workspaceFolders![0].uri.fsPath; const file = join(rootPath, 'extension/managers/ImportManagerFile.tsx'); let document: TextDocument; let documentText: string; @@ -893,10 +893,10 @@ describe('ImportManager with .tsx files', () => { let files: string[]; before(async () => { - const config = new VscodeExtensionConfig(); - files = await findFiles(config, rootPath); + const config = new VscodeExtensionConfig(workspace.workspaceFolders![0].uri); + files = await findFiles(config, workspace.workspaceFolders![0]); - index = index = Container.get(iocSymbols.declarationIndex); + index = new DeclarationIndex(Container.get(iocSymbols.typescriptParser), rootPath); await index.buildIndex(files); document = await workspace.openTextDocument(file); diff --git a/test/single-workspace-tests/index.ts b/test/single-workspace-tests/index.ts new file mode 100644 index 0000000..9956ef9 --- /dev/null +++ b/test/single-workspace-tests/index.ts @@ -0,0 +1,44 @@ +import 'reflect-metadata'; + +import { ExtensionContext, Memento } from 'vscode'; + +import { Container } from '../../src/extension/IoC'; +import { iocSymbols } from '../../src/extension/IoCSymbols'; + +// tslint:disable + +// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testRoot: string, clb: (error:Error) => void) that the extension +// host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +class ContextMock implements ExtensionContext { + subscriptions: { dispose(): any }[] = []; + workspaceState: Memento; + globalState: Memento; + extensionPath: string = ''; + storagePath: string = ''; + asAbsolutePath(): string { + return ''; + } +} + +Container.bind(iocSymbols.extensionContext).toConstantValue(new ContextMock()); + +const testRunner = require('vscode/lib/testrunner'); + +// You can directly control Mocha options by uncommenting the following lines +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info +testRunner.configure({ + ui: 'bdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true // colored output from test results +}); + +module.exports = testRunner;