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();
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 {
return syndicateInfo.get(fileName);
}
@ -132,111 +142,125 @@ const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => {
return withPositions(fileName, [position], kNoInfo, kNoPosition, ([f]) => k(f));
}
function hookHost(host0: ts.CompilerHost | undefined,
options: ts.CompilerOptions)
{
const host = (host0 === void 0) ? ts.createCompilerHost(options, true) : host0;
function compileWithDiagnostics(
name: string,
source: string,
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) {
console.warn('Syndicate plugin refusing to hook CompilerHost twice');
} else {
(host as any).Syndicate_hooked = true;
const oldGetSourceFile = host.getSourceFile;
host.getSourceFile = getSourceFile;
function getSourceFile(fileName: string,
languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void),
shouldCreateNewSourceFile?: boolean): ts.SourceFile | undefined
{
let shouldExpand = false;
syndicateRootDirs.forEach(d => {
if (fileName.startsWith(d)) {
shouldExpand = true;
}
});
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;
function expandFile(
fileName: string,
inputText: string,
makeSourceFile: (expandedText: string) => ts.SourceFile,
): ts.SourceFile {
const { expandedText, sourceFile, targetToSourceMap, sourceToTargetMap, diagnostics } =
compileWithDiagnostics(fileName, inputText, makeSourceFile);
const info = {
sourceFile,
originalSource: inputText,
diagnostics,
targetToSourceMap,
sourceToTargetMap,
};
syndicateInfo.set(fileName, info);
(sourceFile as any).version =
crypto.createHash('sha256').update(expandedText).digest('hex');
return sourceFile;
}
{
const oldCreateProgram = ts.createProgram;
const oldCreateSourceFile = ts.createSourceFile;
function createProgram(createProgramOptions: ts.CreateProgramOptions): ts.Program;
function createProgram(rootNames: readonly string[],
options: ts.CompilerOptions,
host?: ts.CompilerHost,
oldProgram?: ts.Program,
configFileParsingDiagnostics?: readonly ts.Diagnostic[]): ts.Program;
function createProgram(rootNamesOrOptions: readonly string[] | ts.CreateProgramOptions,
options?: ts.CompilerOptions,
host?: ts.CompilerHost,
oldProgram?: ts.Program,
configFileParsingDiagnostics?: readonly ts.Diagnostic[])
: ts.Program
{
if (Array.isArray(rootNamesOrOptions)) {
const rootNames = rootNamesOrOptions;
host = hookHost(host, options!);
return oldCreateProgram(rootNames, options!, host, oldProgram, configFileParsingDiagnostics);
function createSourceFile(
fileName: string,
sourceText: string,
languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions,
setParentNodes?: boolean,
scriptKind?: ts.ScriptKind,
): ts.SourceFile {
const delegate = (text: string) => oldCreateSourceFile(
fileName,
text,
languageVersionOrOptions,
setParentNodes,
scriptKind);
if (shouldExpand(fileName)) {
// console.log('createSourceFile', fileName, sourceText);
return expandFile(fileName, sourceText, delegate);
} else {
const createProgramOptions = rootNamesOrOptions as ts.CreateProgramOptions;
createProgramOptions.host =
hookHost(createProgramOptions.host, createProgramOptions.options);
return oldCreateProgram(createProgramOptions);
return delegate(sourceText);
}
}
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 {
readonly inner: ts.LanguageService;
@ -747,6 +771,31 @@ 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);
}
}
return new SyndicateLanguageService(createInfo.languageService);
}
}