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.
This commit is contained in:
Tony Garnock-Jones 2023-05-02 12:57:22 +03:00
parent 2eb7045b5c
commit a0f87bf687
1 changed files with 103 additions and 55 deletions

View File

@ -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<string, ts.server.ScriptInfo> =
(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<string, ts.server.ScriptInfo> =
// (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);
}
}