diff --git a/.vscode/settings.json b/.vscode/settings.json index 2cf52472..58cff186 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,6 +14,7 @@ "typecheck", "ucfirst", "Uncapitalize", + "unconfigured", "vetur", "vuedx" ], diff --git a/packages/typescript-plugin-vue/src/managers/PluginManager.ts b/packages/typescript-plugin-vue/src/managers/PluginManager.ts index 633f83a2..faf69909 100644 --- a/packages/typescript-plugin-vue/src/managers/PluginManager.ts +++ b/packages/typescript-plugin-vue/src/managers/PluginManager.ts @@ -1,5 +1,4 @@ import { first, setDebugging } from '@vuedx/shared' -import { createHash } from 'crypto' import { Container } from 'inversify' import { TS_LANGUAGE_SERVICE } from '../constants' import type { @@ -28,6 +27,7 @@ export class PluginManager { readonly #containers = new Map() private readonly logger = LoggerService.getLogger(PluginManager.name) + private _activeContainerId: string | undefined public create(options: Options): TSLanguageService { this.#setupLogger(options) @@ -35,17 +35,25 @@ export class PluginManager { return options.languageService } + this.#patchTypescript(options.typescript) + + const containerKey = options.project.getProjectName() + + const container = + this.#containers.get(containerKey) ?? this.#createContainer(options) + + if (this._activeContainerId === containerKey) { + return this.#createLanguageService( + options.languageService, + container.get(TypescriptPluginService), + ) + } + this.logger.debug( 'Creating language service for project:', options.project.getProjectName(), ) - this.#patchTypescript(options.typescript) - - const container = - this.#containers.get(options.project.getProjectName()) ?? - this.#createContainer(options) - container.get(TypescriptContextService).updateOptions(options) this.#patchProject(container, options.project) @@ -54,8 +62,13 @@ export class PluginManager { try { const plugin = container.get(TypescriptPluginService) + plugin.onDispose(() => { + this.#containers.delete(containerKey) + container.unbindAll() + }) return this.#createLanguageService(options.languageService, plugin) } finally { + this._activeContainerId = containerKey const current = ( (options.project.projectService as any) .hostConfiguration as TypeScript.server.HostConfiguration @@ -72,6 +85,7 @@ export class PluginManager { extraFileExtensions: [], }) } + this._activeContainerId = undefined } } @@ -120,6 +134,7 @@ export class PluginManager { #patchProject(container: Container, project: TSProject): void { const ts = container.get(TypescriptContextService) + const fs = container.get(FilesystemService) const logger = LoggerService.getLogger('Project') overrideMethod( @@ -179,6 +194,35 @@ export class PluginManager { } }, ) + + overrideMethod( + project as unknown as { + detachScriptInfoFromProject( + uncheckedFileName: string, + noRemoveResolution?: boolean, + ): void + }, + 'detachScriptInfoFromProject', + (detachScriptInfoFromProject) => + (uncheckedFileName, noRemoveResolution) => { + if (fs.isVueFile(uncheckedFileName)) return + if (fs.isGeneratedVueFile(uncheckedFileName)) { + const fileName = fs.getRealFileNameIfAny(uncheckedFileName) + console.debug(`@@@ Detaching ${fileName}`) + return detachScriptInfoFromProject.call( + project, + fileName, + noRemoveResolution, + ) + } + + return detachScriptInfoFromProject.call( + project, + uncheckedFileName, + noRemoveResolution, + ) + }, + ) } #patchServerHost(container: Container, serverHost: TSServerHost): void { @@ -311,7 +355,8 @@ export class PluginManager { containingFile, reusedNames, redirectedReference, - _options, + options, + containingSourceFile, ) => { if (fs.isVueRuntimeFile(containingFile)) { const anyProjectFile = first(ts.project.getRootFiles()) @@ -320,7 +365,7 @@ export class PluginManager { const core = ts.lib.resolveModuleName( '@vue/runtime-core', anyProjectFile, - _options, + options, ts.serverHost, undefined, redirectedReference, @@ -328,7 +373,7 @@ export class PluginManager { const vue = ts.lib.resolveModuleName( 'vue', anyProjectFile, - _options, + options, ts.serverHost, undefined, redirectedReference, @@ -352,13 +397,15 @@ export class PluginManager { containingFile, reusedNames, redirectedReference, - _options, + options, ) : ts.project.resolveModuleNames( moduleNames, containingFile, reusedNames, redirectedReference, + options, + containingSourceFile, ) const known = { @@ -373,7 +420,8 @@ export class PluginManager { } moduleNames.forEach((name, index) => { const handler = known[name as keyof typeof known] - if (handler != null && result[index] == null) { + const resolved = result[index] + if (handler != null && resolved == null) { result[index] = handler() } }) @@ -409,11 +457,13 @@ export class PluginManager { return container } + readonly #loggerIds = new Map() #setupLogger(options: Options): void { - const id = createHash('md5') - .update(options.project.getProjectName()) - .digest('hex') - .slice(0, 6) + const id = + this.#loggerIds.get(options.project.getProjectName()) ?? + `${this.#loggerIds.size}` + + this.#loggerIds.set(options.project.getProjectName(), id) if (LoggerService.currentId === id) return const logger = options.project.projectService.logger @@ -464,7 +514,6 @@ export class PluginManager { has: (target, prop) => { return prop === TS_LANGUAGE_SERVICE || prop in target }, - // TODO: Implement set? }) } } diff --git a/packages/typescript-plugin-vue/src/services/TypescriptPluginService.ts b/packages/typescript-plugin-vue/src/services/TypescriptPluginService.ts index c4d66abf..ac82e0eb 100644 --- a/packages/typescript-plugin-vue/src/services/TypescriptPluginService.ts +++ b/packages/typescript-plugin-vue/src/services/TypescriptPluginService.ts @@ -63,19 +63,7 @@ export class TypescriptPluginService private readonly ipc: IPCService, ) { if (Math.random() > 1) { - console.log([ - this.classifications, - this.codeFix, - this.completions, - this.definitions, - this.folding, - this.implementation, - this.quickInfo, - this.refactor, - this.references, - this.rename, - this.signature, - ]) + console.log([this.classifications, this.folding, this.implementation]) } } //#endregion @@ -139,6 +127,20 @@ export class TypescriptPluginService const vue = new Set() const virtual = new Set() + // This is not needed for any functionality, but it's needed to prevent unnecessary creation of inferred project. + this.ts.projectService.openFiles.forEach((_, file) => { + const scriptInfo = this.ts.projectService.getScriptInfoForPath( + file as TypeScript.Path, + ) + if (scriptInfo == null) return + if ( + scriptInfo.containingProjects.length === 0 || // creating new project, so this is likely to be part of current project. TODO: verify hypothesis. + scriptInfo.containingProjects.includes(this.ts.project) + ) { + all.push(scriptInfo.fileName) + } + }) + if (this.ts.isConfiguredProject(this.ts.project)) { const options = this.ts.project.getParsedCommandLine?.( this.ts.project.getConfigFilePath(), @@ -161,23 +163,15 @@ export class TypescriptPluginService if (vue.size === 0) { this.#isVueProject = false - this.logger.debug('Not a Vue project') + this.logger.debug('Not a Vue project:', this.ts.project.getProjectName()) return [] // do not retain any files if no .vue files } this.#isVueProject = true const fileNames = [...this.getScriptFileNames([...vue]), ...virtual] - this.logger.debug(`Project: ${this.ts.project.getProjectName()}`) + this.logger.debug(`Project:`, this.ts.project.getProjectName()) this.logger.debug(`External files:`, fileNames) - this.logger.debug( - 'Open external files:', - fileNames.filter((fileName) => - this.ts.projectService.openFiles.has( - this.ts.toNormalizedPath(fileName), - ), - ), - ) return fileNames } @@ -958,8 +952,15 @@ export class TypescriptPluginService return this.ts.service.getTodoComments(fileName, descriptors) } + readonly #disposables: Array<() => void> = [] + + public onDispose(callback: () => void): void { + this.#disposables.push(callback) + } + public dispose(): void { this.ipc.dispose() + this.#disposables.forEach((dispose) => dispose()) this.ts.service.dispose() } diff --git a/samples/vue3/javascript-configured-include-directory/src/main.js b/samples/vue3/javascript-configured-include-directory/src/main.js new file mode 100644 index 00000000..e69de29b diff --git a/samples/vue3/typescript-configured-include-directory/src/main.ts b/samples/vue3/typescript-configured-include-directory/src/main.ts new file mode 100644 index 00000000..e69de29b diff --git a/test/specs/resolutions.spec.ts b/test/specs/resolutions.spec.ts index a1256d4f..952d4d8f 100644 --- a/test/specs/resolutions.spec.ts +++ b/test/specs/resolutions.spec.ts @@ -1,5 +1,3 @@ -// TODO: Why is this so slow? - import { createEditorContext, getProjectPath } from '../support/helpers' import { TestServer } from '../support/TestServer' const data = { @@ -14,88 +12,117 @@ const data = { 'javascript-configured-include-file', ], } as const -describe.each(['js', 'ts'] as const)('project', (ext) => { + +describe('resolutions', () => { const server = new TestServer() afterAll(async () => await server.close()) - const projects = data[ext] - - describe.each(projects)('%s', (project) => { - const editor = createEditorContext(server, getProjectPath(project)) - afterEach(async () => await editor.closeAll()) - - test('finds .vue root files when a .ts file from project is opened', async () => { - await editor.open(`src/a.${ext}`) - const projectInfo = await editor.getProjectInfo(`src/a.${ext}`) - - expect(projectInfo.fileNames).toMatchObject( - expect.arrayContaining([ - editor.abs(`src/a.${ext}`), - editor.abs(`src/b.${ext}`), - editor.abs('src/A.vue.tsx'), - editor.abs('src/B.vue.tsx'), - ]), - ) - }) - - test('finds .vue root files when a .vue file from project is opened', async () => { - await editor.open('src/A.vue') - const projectInfo = await editor.getProjectInfo('src/A.vue') - - expect(projectInfo.fileNames).toMatchObject( - expect.arrayContaining([ - editor.abs(`src/a.${ext}`), - editor.abs(`src/b.${ext}`), - editor.abs('src/A.vue.tsx'), - editor.abs('src/B.vue.tsx'), - ]), - ) - }) - - test(`resolves .vue imports in .${ext} files`, async () => { - await editor.open(`src/a.${ext}`) - const diagnostics = await editor.getDiagnostics(`src/a.${ext}`) - - expect(diagnostics.semantic).toHaveLength(0) - }) - - test('resolves .vue imports in .vue files', async () => { - await editor.open('src/A.vue') - const diagnostics = await editor.getDiagnostics('src/A.vue') - - expect(diagnostics.semantic).toHaveLength(0) - }) - - test('no compler options issues', async () => { - await editor.open('src/A.vue') - const diagnostics = await editor.getCompilerDiagnostics('src/A.vue') - expect(diagnostics).toHaveLength(0) - }) - }) - - describe.each(projects)('%s', (project) => { - if (!/^(java|type)script-configured-include-file$/.test(project)) return - const editor = createEditorContext(server, getProjectPath(project)) - afterEach(async () => await editor.closeAll()) - test('finds .vue root files when entry file is opened', async () => { - await editor.open(`src/main.${ext}`) - const projectInfo = await editor.getProjectInfo(`src/main.${ext}`) - - expect(projectInfo.fileNames).toMatchObject( - expect.arrayContaining([ - editor.abs(`src/a.${ext}`), - editor.abs(`src/b.${ext}`), - editor.abs('src/A.vue.tsx'), - editor.abs('src/B.vue.tsx'), - ]), - ) - }) + describe.each(Object.keys(data) as Array)( + 'project', + (ext) => { + const projects = data[ext] + describe.each(projects)('%s', (project) => { + const editor = createEditorContext(server, getProjectPath(project)) + afterEach(async () => { + await editor.closeAll() + await server.flush() + }) + + test('finds .vue root files when a .ts file from project is opened', async () => { + await editor.open(`src/a.${ext}`) + const projectInfo = await editor.getProjectInfo(`src/a.${ext}`) + + expect(projectInfo.fileNames).toMatchObject( + expect.arrayContaining([ + editor.abs(`src/a.${ext}`), + editor.abs(`src/b.${ext}`), + editor.abs('src/A.vue.tsx'), + editor.abs('src/B.vue.tsx'), + ]), + ) + }) + + test('finds .vue root files when a .vue file from project is opened', async () => { + await editor.open('src/A.vue') + const projectInfo = await editor.getProjectInfo('src/A.vue') + + expect(projectInfo.fileNames).toMatchObject( + expect.arrayContaining([ + editor.abs(`src/a.${ext}`), + editor.abs(`src/b.${ext}`), + editor.abs('src/A.vue.tsx'), + editor.abs('src/B.vue.tsx'), + ]), + ) + }) + + test(`resolves .vue imports in .${ext} files`, async () => { + await editor.open(`src/a.${ext}`) + const diagnostics = await editor.getDiagnostics(`src/a.${ext}`) + + expect(diagnostics.semantic).toHaveLength(0) + }) + + test('resolves .vue imports in .vue files', async () => { + await editor.open('src/A.vue') + const diagnostics = await editor.getDiagnostics('src/A.vue') + + expect(diagnostics.semantic).toHaveLength(0) + }) + + test('no complier options issues', async () => { + await editor.open('src/A.vue') + const diagnostics = await editor.getCompilerDiagnostics('src/A.vue') + expect(diagnostics).toHaveLength(0) + }) + + test('detects config file', async () => { + await editor.open('src/A.vue') + const projectInfo = await editor.getProjectInfo('src/A.vue.tsx') + if (project.includes('unconfigured')) { + expect(projectInfo.configFileName).toEqual( + expect.stringContaining('/dev/null/inferredProject'), + ) + } else { + expect(projectInfo.configFileName).toBe( + editor.abs(ext + 'config.json'), + ) + } + }) + + test('finds .vue root files when entry file is opened', async () => { + await editor.open(`src/main.${ext}`) + const projectInfo = await editor.getProjectInfo(`src/main.${ext}`) + + expect(projectInfo.fileNames).toMatchObject( + expect.arrayContaining([ + editor.abs(`src/a.${ext}`), + editor.abs(`src/b.${ext}`), + editor.abs('src/A.vue.tsx'), + editor.abs('src/B.vue.tsx'), + ]), + ) + }) + }) + }, + ) + + test('resolve .vue file from .ts entry file', async () => { + const editor = createEditorContext( + server, + getProjectPath('typescript-configured-include-file'), + ) + await editor.open(`src/main.ts`) + const projectInfo = await editor.getProjectInfo(`src/main.ts`) + expect(projectInfo.fileNames).toContain(editor.abs('src/main.ts')) + expect(projectInfo.fileNames).toContain(editor.abs('src/A.vue.tsx')) }) }) -// TODO: Does not work when includes is a ts file. -test.skip('open two projects', async () => { +describe('multiple projects', () => { const server = new TestServer() - try { + afterAll(async () => await server.close()) + + test('open two projects', async () => { const a = createEditorContext( server, getProjectPath('typescript-configured-include-directory'), @@ -135,7 +162,5 @@ test.skip('open two projects', async () => { ]), ) } - } finally { - await server.close() - } + }) })