From a0f87bf687a5ce0959db42c33edaeefde2d22143 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Tue, 2 May 2023 12:57:22 +0300 Subject: [PATCH] Monkeypatch ScriptInfo instead of subclassing-and-replacing. This means we can ditch the whole approach of trying to remove stale ScriptInfo instances at plugin startup, instead just endowing existing ones with new behaviour. This repairs an error where renaming an identifer would perform spurious edits. --- packages/ts-plugin/src/index.ts | 158 +++++++++++++++++++++----------- 1 file changed, 103 insertions(+), 55 deletions(-) diff --git a/packages/ts-plugin/src/index.ts b/packages/ts-plugin/src/index.ts index 56f8631..184dddb 100644 --- a/packages/ts-plugin/src/index.ts +++ b/packages/ts-plugin/src/index.ts @@ -229,37 +229,55 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => { ts.createSourceFile = createSourceFile; } - class SyndicateScriptInfo extends ts.server.ScriptInfo { - constructor( - host: ts.server.ServerHost, - fileName: ts.server.NormalizedPath, - scriptKind: ts.ScriptKind, - hasMixedContent: boolean, - path: ts.Path, - initialVersion?: ts.server.ScriptInfoVersion, - ) { - if (shouldExpand(fileName)) { - console.log('SyndicateScriptInfo constructor', fileName); - } - super(host, fileName, scriptKind, hasMixedContent, path, initialVersion); - } - - editContent(start: number, end: number, newText: string): void { - // console.log('SyndicateScriptInfo.editContent', this.fileName, start, end, newText); - withFileName(this.fileName, - () => super.editContent(start, end, newText), - (fx) => { - const info = fx.info; - const revised = info.originalSource.substring(0, start) + - newText + - info.originalSource.substring(end); - info.originalSource = revised; - this.open(revised); - }); - } + { + const oldEditContent = ts.server.ScriptInfo.prototype.editContent; + ts.server.ScriptInfo.prototype.editContent = + function (start: number, end: number, newText: string): void { + // console.log('SyndicateScriptInfo.editContent', this.fileName, start, end, newText); + withFileName(this.fileName, + () => oldEditContent(start, end, newText), + (fx) => { + const info = fx.info; + const revised = info.originalSource.substring(0, start) + + newText + + info.originalSource.substring(end); + info.originalSource = revised; + this.open(revised); + }); + }; } - ts.server.ScriptInfo = SyndicateScriptInfo; + // class SyndicateScriptInfo extends ts.server.ScriptInfo { + // constructor( + // host: ts.server.ServerHost, + // fileName: ts.server.NormalizedPath, + // scriptKind: ts.ScriptKind, + // hasMixedContent: boolean, + // path: ts.Path, + // initialVersion?: ts.server.ScriptInfoVersion, + // ) { + // if (shouldExpand(fileName)) { + // console.log('SyndicateScriptInfo constructor', fileName); + // } + // super(host, fileName, scriptKind, hasMixedContent, path, initialVersion); + // } + // + // editContent(start: number, end: number, newText: string): void { + // // console.log('SyndicateScriptInfo.editContent', this.fileName, start, end, newText); + // withFileName(this.fileName, + // () => super.editContent(start, end, newText), + // (fx) => { + // const info = fx.info; + // const revised = info.originalSource.substring(0, start) + + // newText + + // info.originalSource.substring(end); + // info.originalSource = revised; + // this.open(revised); + // }); + // } + // } + // + // ts.server.ScriptInfo = SyndicateScriptInfo; class SyndicateLanguageService implements ts.LanguageService { readonly inner: ts.LanguageService; @@ -761,7 +779,9 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => { class SyndicatePlugin implements ts.server.PluginModule { create(createInfo: ts.server.PluginCreateInfo): ts.LanguageService { - const options = createInfo.project.getCompilerOptions(); + const project = createInfo.project; + + const options = project.getCompilerOptions(); if (options.rootDir !== void 0) { syndicateRootDirs.add(finalSlash(options.rootDir)); } @@ -771,31 +791,59 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => { if (options.rootDir === void 0 && options.rootDirs === void 0) { syndicateRootDirs.add(finalSlash(path.resolve('.'))); } - // This next little block is required to flush out previously-created ScriptInfo - // instances for our project, since they were created before our plugin was even - // loaded and our plugin monkey-patches ScriptInfo to SyndicateScriptInfo. - { - const infos: Map = - (createInfo.project.projectService as any).filenameToScriptInfo; - const oldInfos = Array.from(infos.values()); - infos.clear(); - for (const info of oldInfos) { - // console.log('---', info.fileName); - // console.log(JSON.stringify({ - // oldText: (info as any).textStorage.text, - // }, void 0, 2)); - const newInfo = new SyndicateScriptInfo( - (info as any).host, - info.fileName, - info.scriptKind, - info.hasMixedContent, - info.path, - (info as any).textStorage.version); - (newInfo as any).textStorage = (info as any).textStorage; - infos.set(info.fileName, newInfo); - createInfo.project.getSourceFile(info.fileName as any); - } - } + + // // This next block is required to flush out previously-created ScriptInfo instances + // // for our project, since they were created before our plugin was even loaded and + // // our plugin monkey-patches ScriptInfo to SyndicateScriptInfo. + // { + // const infos: Map = + // (project.projectService as any).filenameToScriptInfo; + // const oldInfos = Array.from(infos.values()); + + // infos.clear(); + // for (const info of oldInfos) { + + // if (!shouldExpand(info.fileName)) { + // continue; + // } + + // const existingText = (info as any).textStorage.text; + + // console.log('---', JSON.stringify({ + // fileName: info.fileName, + // existingText, + // containingProjects: info.containingProjects.map(p => p.getProjectName()), + // }, void 0, 2)); + + // const newInfo = new SyndicateScriptInfo( + // (info as any).host, + // info.fileName, + // info.scriptKind, + // info.hasMixedContent, + // info.path, + // (info as any).textStorage.version); + // (newInfo as any).textStorage = (info as any).textStorage; + // infos.set(info.fileName, newInfo); + // // ^ transfers necessary live information from old ScriptInfo instances to + // // new SyndicateScriptInfo instances, so open files with syntax errors keep + // // their syntax error info. + + // info.close(); + // // ^ detaches old ScriptInfo from projects, so we don't end up with bizarre + // // duplicates in e.g. automated renamings. + + // info.detachAllProjects(); + // // info.detachFromProject(project); + // newInfo.attachToProject(project); + + // newInfo.open(existingText); + // // ^ flag the info as open again, so that a (re)compilation happens and any + // // existing syntax errors actually show up + + // // project.getSourceFile(info.fileName as any); + // } + // } + return new SyndicateLanguageService(createInfo.languageService); } }