Fix up a few of the more egregious problems with the plugin.

TypeScript has been moving on while the plugin has been staying still, and
that has caused the plugin to stop working well. This patch gets things
back into somewhat workable state, but I'm sure more will be required.

Changes include:

 - `hookHost` is no longer required: instead, we hook ts.createSourceFile.

 - `shouldExpand` abstracts away details of whether a file is considered
   Syndicateish or not.

 - The code that does the Syndicate expansion has been abstracted out of
   the detail of how expansion is invoked by the language server.

 - I've had to monkey-patch ScriptInfo in order to get access to the
   `editContent` method.

 - As a consequence, at plugin startup, we invalidate existing ScriptInfo
   instances so they are rebuilt with our SyndicateScriptInfo constructor.
This commit is contained in:
Tony Garnock-Jones 2023-04-28 10:32:49 +02:00
parent 3384acbd62
commit 075893fc85
1 changed files with 143 additions and 94 deletions

View File

@ -20,6 +20,16 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => {
const syndicateRootDirs: Set<string> = new Set(); const syndicateRootDirs: Set<string> = new Set();
function shouldExpand(fileName: string): boolean {
let result = false;
syndicateRootDirs.forEach(d => {
if (fileName.startsWith(d)) {
result = true;
}
});
return result;
}
function getInfo(fileName: string): SyndicateInfo | undefined { function getInfo(fileName: string): SyndicateInfo | undefined {
return syndicateInfo.get(fileName); return syndicateInfo.get(fileName);
} }
@ -132,111 +142,125 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => {
return withPositions(fileName, [position], kNoInfo, kNoPosition, ([f]) => k(f)); return withPositions(fileName, [position], kNoInfo, kNoPosition, ([f]) => k(f));
} }
function hookHost(host0: ts.CompilerHost | undefined, function compileWithDiagnostics(
options: ts.CompilerOptions) name: string,
{ source: string,
const host = (host0 === void 0) ? ts.createCompilerHost(options, true) : host0; makeSourceFile: (expandedText: string) => ts.SourceFile,
): {
expandedText: string,
targetToSourceMap: Syntax.SpanIndex<Syntax.Token>,
sourceToTargetMap: Syntax.SpanIndex<number>,
sourceFile: ts.SourceFile,
diagnostics: ts.DiagnosticWithLocation[],
} {
const diagnostics: Array<{ message: string, start?: Syntax.Pos, end?: Syntax.Pos }> = [];
// console.log(`Syndicate expanding ${name}: ${JSON.stringify(source)}`);
console.log(`Syndicate expanding ${name}`);
const { text: expandedText, targetToSourceMap, sourceToTargetMap } = compile({
source,
name,
typescript: true,
emitError: (message, start, end) => {
console.error(`${Syntax.formatPos(start)}-${Syntax.formatPos(end)}: ${message}`);
diagnostics.push({ message, start, end });
},
});
const sourceFile = makeSourceFile(expandedText);
return {
expandedText,
targetToSourceMap,
sourceToTargetMap,
sourceFile,
diagnostics: diagnostics.map(({ message, start, end }) => ({
category: ts.DiagnosticCategory.Error,
code: 99999,
file: sourceFile,
start: start?.pos ?? 0,
length: Math.max(0, (end?.pos ?? 0) - (start?.pos ?? 0)),
messageText: message,
})),
};
}
if ('Syndicate_hooked' in host) { function expandFile(
console.warn('Syndicate plugin refusing to hook CompilerHost twice'); fileName: string,
} else { inputText: string,
(host as any).Syndicate_hooked = true; makeSourceFile: (expandedText: string) => ts.SourceFile,
): ts.SourceFile {
const oldGetSourceFile = host.getSourceFile; const { expandedText, sourceFile, targetToSourceMap, sourceToTargetMap, diagnostics } =
host.getSourceFile = getSourceFile; compileWithDiagnostics(fileName, inputText, makeSourceFile);
const info = {
function getSourceFile(fileName: string, sourceFile,
languageVersion: ts.ScriptTarget, originalSource: inputText,
onError?: ((message: string) => void), diagnostics,
shouldCreateNewSourceFile?: boolean): ts.SourceFile | undefined targetToSourceMap,
{ sourceToTargetMap,
let shouldExpand = false; };
syndicateRootDirs.forEach(d => { syndicateInfo.set(fileName, info);
if (fileName.startsWith(d)) { (sourceFile as any).version =
shouldExpand = true; crypto.createHash('sha256').update(expandedText).digest('hex');
} return sourceFile;
});
if (shouldExpand) {
try {
const inputText = host.readFile(fileName);
if (inputText === void 0) {
onError?.(`Could not read input file ${fileName}`);
return undefined;
}
const diagnostics:
Array<{ message: string, start?: Syntax.Pos, end?: Syntax.Pos }> = [];
console.log('Syndicate compiling', fileName);
const { text: expandedText, targetToSourceMap, sourceToTargetMap } = compile({
source: inputText,
name: fileName,
typescript: true,
emitError: (message, start, end) => {
console.error(`${Syntax.formatPos(start)}-${Syntax.formatPos(end)}: ${message}`);
diagnostics.push({ message, start, end });
},
});
const sf = ts.createSourceFile(fileName, expandedText, languageVersion, true);
syndicateInfo.set(fileName, {
sourceFile: sf,
originalSource: inputText,
diagnostics: diagnostics.map(({ message, start, end }) => ({
category: ts.DiagnosticCategory.Error,
code: 99999,
file: sf,
start: start?.pos ?? 0,
length: Math.max(0, (end?.pos ?? 0) - (start?.pos ?? 0)),
messageText: message,
})),
targetToSourceMap,
sourceToTargetMap,
});
(sf as any).version = crypto.createHash('sha256').update(expandedText).digest('hex');
return sf;
} catch (e) {
console.error(e);
onError?.((e as any).message ?? '<no error message available>');
return undefined;
}
} else {
return oldGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
}
}
}
return host;
} }
{ {
const oldCreateProgram = ts.createProgram; const oldCreateSourceFile = ts.createSourceFile;
function createProgram(createProgramOptions: ts.CreateProgramOptions): ts.Program; function createSourceFile(
function createProgram(rootNames: readonly string[], fileName: string,
options: ts.CompilerOptions, sourceText: string,
host?: ts.CompilerHost, languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions,
oldProgram?: ts.Program, setParentNodes?: boolean,
configFileParsingDiagnostics?: readonly ts.Diagnostic[]): ts.Program; scriptKind?: ts.ScriptKind,
function createProgram(rootNamesOrOptions: readonly string[] | ts.CreateProgramOptions, ): ts.SourceFile {
options?: ts.CompilerOptions, const delegate = (text: string) => oldCreateSourceFile(
host?: ts.CompilerHost, fileName,
oldProgram?: ts.Program, text,
configFileParsingDiagnostics?: readonly ts.Diagnostic[]) languageVersionOrOptions,
: ts.Program setParentNodes,
{ scriptKind);
if (Array.isArray(rootNamesOrOptions)) { if (shouldExpand(fileName)) {
const rootNames = rootNamesOrOptions; // console.log('createSourceFile', fileName, sourceText);
host = hookHost(host, options!); return expandFile(fileName, sourceText, delegate);
return oldCreateProgram(rootNames, options!, host, oldProgram, configFileParsingDiagnostics);
} else { } else {
const createProgramOptions = rootNamesOrOptions as ts.CreateProgramOptions; return delegate(sourceText);
createProgramOptions.host =
hookHost(createProgramOptions.host, createProgramOptions.options);
return oldCreateProgram(createProgramOptions);
} }
} }
ts.createProgram = createProgram; 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);
});
}
}
ts.server.ScriptInfo = SyndicateScriptInfo;
class SyndicateLanguageService implements ts.LanguageService { class SyndicateLanguageService implements ts.LanguageService {
readonly inner: ts.LanguageService; readonly inner: ts.LanguageService;
@ -747,6 +771,31 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => {
if (options.rootDir === void 0 && options.rootDirs === void 0) { if (options.rootDir === void 0 && options.rootDirs === void 0) {
syndicateRootDirs.add(finalSlash(path.resolve('.'))); 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);
}
}
return new SyndicateLanguageService(createInfo.languageService); return new SyndicateLanguageService(createInfo.languageService);
} }
} }