936 lines
41 KiB
TypeScript
936 lines
41 KiB
TypeScript
/// SPDX-License-Identifier: GPL-3.0-or-later
|
|
/// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
|
|
|
import { compile, Syntax } from '@syndicate-lang/compiler';
|
|
import tslib from 'typescript/lib/tsserverlibrary';
|
|
import crypto from 'crypto';
|
|
import path from 'path';
|
|
|
|
const boot: tslib.server.PluginModuleFactory = ({ typescript: ts }) => {
|
|
|
|
interface SyndicateInfo {
|
|
sourceFile: ts.SourceFile;
|
|
originalSource: string;
|
|
diagnostics: ts.DiagnosticWithLocation[];
|
|
targetToSourceMap: Syntax.SpanIndex<Syntax.Token>;
|
|
sourceToTargetMap: Syntax.SpanIndex<number>;
|
|
}
|
|
|
|
const syndicateInfo: Map<string, SyndicateInfo> = new Map();
|
|
|
|
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);
|
|
}
|
|
|
|
function fixupDocumentSpan(loc: ts.DocumentSpan | undefined): ts.DocumentSpan | undefined {
|
|
if (loc !== void 0) {
|
|
withFileName(loc.fileName, () => void 0, (fixupLoc) => {
|
|
fixupLoc.span(loc.textSpan);
|
|
fixupLoc.span(loc.contextSpan);
|
|
});
|
|
withFileName(loc.originalFileName, () => void 0, (fixupOriginal) => {
|
|
fixupOriginal.span(loc.originalTextSpan);
|
|
fixupOriginal.span(loc.originalContextSpan);
|
|
});
|
|
}
|
|
return loc;
|
|
}
|
|
|
|
class Fixup {
|
|
readonly info: SyndicateInfo;
|
|
|
|
constructor(info: SyndicateInfo) {
|
|
this.info = info;
|
|
}
|
|
|
|
loc(start: number): number | undefined {
|
|
const p = this.info.targetToSourceMap.get(start);
|
|
if (p === null) return undefined;
|
|
return p.firstItem.start.pos + p.offset;
|
|
}
|
|
|
|
span(s: ts.TextSpan): ts.TextSpan;
|
|
span(s: undefined): undefined;
|
|
span(s: ts.TextSpan | undefined): ts.TextSpan | undefined;
|
|
span(s: ts.TextSpan | undefined): ts.TextSpan | undefined {
|
|
if (s !== void 0 && !('__syndicate_translated__' in s)) {
|
|
const newStart = this.loc(s.start);
|
|
if (newStart === void 0) throw new Error(
|
|
"Source position unavailable for TextSpan " + JSON.stringify(s));
|
|
s.start = newStart;
|
|
(s as any).__syndicate_translated__ = true;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
diagnostic<T extends ts.Diagnostic>(d: T, ds: T[]) {
|
|
if (d.start !== void 0 && !('__syndicate_translated__' in d)) {
|
|
const p = this.info.targetToSourceMap.get(d.start);
|
|
if (p === null) return;
|
|
if (p.firstItem.synthetic) return;
|
|
d.start = p.firstItem.start.pos + p.offset;
|
|
(d as any).__syndicate_translated__ = true;
|
|
}
|
|
ds.push(d);
|
|
}
|
|
|
|
diagnostics<T extends ts.Diagnostic>(ds: T[]): T[] {
|
|
const vs: T[] = [];
|
|
ds.forEach(d => this.diagnostic(d, vs));
|
|
return vs;
|
|
}
|
|
}
|
|
|
|
class PositionFixup extends Fixup {
|
|
readonly target: Syntax.SpanResult<number>;
|
|
|
|
constructor(info: SyndicateInfo, target: Syntax.SpanResult<number>) {
|
|
super(info);
|
|
this.target = target;
|
|
}
|
|
|
|
get targetStart(): number {
|
|
return this.target.firstItem + this.target.offset;
|
|
}
|
|
|
|
get targetEnd(): number {
|
|
return this.target.lastItem + this.target.offset;
|
|
}
|
|
}
|
|
|
|
function withFileName<T>(fileName: string | undefined,
|
|
kNoInfo: () => T,
|
|
k: (f: Fixup) => T): T
|
|
{
|
|
if (fileName === void 0) return kNoInfo();
|
|
const info = getInfo(fileName);
|
|
if (info === void 0) return kNoInfo();
|
|
return k(new Fixup(info));
|
|
}
|
|
|
|
function withPositions<T>(fileName: string,
|
|
positions: Array<number>,
|
|
kNoInfo: () => T,
|
|
kNoPosition: () => T,
|
|
k: (f: Array<PositionFixup>) => T): T
|
|
{
|
|
return withFileName(fileName, kNoInfo, (fx) => {
|
|
const t = positions.map(p => fx.info.sourceToTargetMap.get(p));
|
|
if (t.some(p => p === null)) return kNoPosition();
|
|
return k(t.map(p => new PositionFixup(fx.info, p!)));
|
|
});
|
|
}
|
|
|
|
function withPosition<T>(fileName: string,
|
|
position: number,
|
|
kNoInfo: () => T,
|
|
kNoPosition: () => T,
|
|
k: (f: PositionFixup) => T): T
|
|
{
|
|
return withPositions(fileName, [position], kNoInfo, kNoPosition, ([f]) => k(f));
|
|
}
|
|
|
|
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 startTime = +new Date();
|
|
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 stopTime = +new Date();
|
|
console.log(`Syndicate expansion of ${name} took ${stopTime - startTime} ms`);
|
|
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,
|
|
})),
|
|
};
|
|
}
|
|
|
|
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 oldCreateSourceFile = ts.createSourceFile;
|
|
|
|
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 {
|
|
return delegate(sourceText);
|
|
}
|
|
}
|
|
|
|
ts.createSourceFile = createSourceFile;
|
|
}
|
|
|
|
{
|
|
const oldUpdateSourceFile = ts.updateSourceFile;
|
|
|
|
function commonPrefixLength(a: string, b: string): number {
|
|
const limit = Math.min(a.length, b.length);
|
|
for (let i = 0; i < limit; i++) {
|
|
if (a[i] !== b[i]) return i;
|
|
}
|
|
return limit;
|
|
}
|
|
|
|
function commonSuffixLength(a: string, b: string): number {
|
|
const limit = Math.min(a.length, b.length);
|
|
for (let i = 0; i < limit; i++) {
|
|
if (a[a.length - i - 1] !== b[b.length - i - 1]) return i;
|
|
}
|
|
return limit;
|
|
}
|
|
|
|
function updateSourceFile(
|
|
sourceFile: ts.SourceFile,
|
|
newText: string,
|
|
textChangeRange: ts.TextChangeRange,
|
|
aggressiveChecks?: boolean,
|
|
): ts.SourceFile {
|
|
return withFileName(
|
|
sourceFile.fileName,
|
|
() => oldUpdateSourceFile(sourceFile, newText, textChangeRange, aggressiveChecks),
|
|
(fx) => {
|
|
const oldInfo = fx.info;
|
|
return expandFile(
|
|
sourceFile.fileName,
|
|
oldInfo.originalSource,
|
|
(newExpandedText) => {
|
|
const oldExpandedText = oldInfo.sourceFile.text;
|
|
if (oldExpandedText === newExpandedText) {
|
|
// console.log('updateSourceFile', 'no change');
|
|
return oldInfo.sourceFile;
|
|
} else {
|
|
const prefix = commonPrefixLength(oldExpandedText, newExpandedText);
|
|
const suffix =
|
|
commonSuffixLength(oldExpandedText.substring(prefix),
|
|
newExpandedText.substring(prefix));
|
|
const newChangeRange = {
|
|
span: {
|
|
start: prefix,
|
|
length: oldExpandedText.length - prefix - suffix,
|
|
},
|
|
newLength: newExpandedText.length - prefix - suffix,
|
|
} satisfies ts.TextChangeRange;
|
|
// console.log('updateSourceFile', JSON.stringify({
|
|
// oldSyndicateText: oldInfo.originalSource,
|
|
// newSyndicateText: newText,
|
|
// oldExpandedText,
|
|
// newExpandedText,
|
|
// oldLength: oldExpandedText.length,
|
|
// newLength: newExpandedText.length,
|
|
// oldChangeRange: textChangeRange,
|
|
// newChangeRange,
|
|
// prefix,
|
|
// suffix,
|
|
// }, void 0, 2));
|
|
return oldUpdateSourceFile(
|
|
sourceFile, newExpandedText, newChangeRange, aggressiveChecks);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
ts.updateSourceFile = updateSourceFile;
|
|
}
|
|
|
|
{
|
|
const oldEditContent = ts.server.ScriptInfo.prototype.editContent;
|
|
ts.server.ScriptInfo.prototype.editContent =
|
|
function (start: number, end: number, newText: string): void {
|
|
// console.log('SyndicateScriptInfo.editContent', JSON.stringify({
|
|
// fileName: this.fileName,
|
|
// start,
|
|
// end,
|
|
// newText,
|
|
// oldText: (this as any).textStorage.text ?? ["MISSING"],
|
|
// }, void 0, 2));
|
|
withFileName(this.fileName,
|
|
() => oldEditContent.call(this, start, end, newText),
|
|
(fx) => {
|
|
const info = fx.info;
|
|
const revised = info.originalSource.substring(0, start) +
|
|
newText +
|
|
info.originalSource.substring(end);
|
|
info.originalSource = revised;
|
|
oldEditContent.call(this, start, end, newText);
|
|
});
|
|
};
|
|
}
|
|
|
|
// 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;
|
|
|
|
constructor(inner: ts.LanguageService) {
|
|
this.inner = inner;
|
|
}
|
|
|
|
provideInlayHints(fileName: string, span: ts.TextSpan, preferences: ts.UserPreferences | undefined): ts.InlayHint[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getFileReferences(fileName: string): tslib.ReferenceEntry[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
cleanupSemanticCache(): void {
|
|
}
|
|
|
|
getSyntacticDiagnostics(fileName: string): ts.DiagnosticWithLocation[] {
|
|
const ds = this.inner.getSyntacticDiagnostics(fileName);
|
|
return withFileName(fileName, () => ds, (fixup) => {
|
|
const fixedUp = fixup.diagnostics(ds);
|
|
fixedUp.push(... fixup.info.diagnostics);
|
|
return fixedUp;
|
|
});
|
|
}
|
|
|
|
getSemanticDiagnostics(fileName: string): ts.Diagnostic[] {
|
|
const ds = this.inner.getSemanticDiagnostics(fileName);
|
|
return withFileName(fileName, () => ds, (fixup) => fixup.diagnostics(ds));
|
|
}
|
|
|
|
getSuggestionDiagnostics(fileName: string): ts.DiagnosticWithLocation[] {
|
|
const ds = this.inner.getSuggestionDiagnostics(fileName);
|
|
return withFileName(fileName, () => ds, (fixup) => fixup.diagnostics(ds));
|
|
}
|
|
|
|
getCompilerOptionsDiagnostics(): ts.Diagnostic[] {
|
|
return this.inner.getCompilerOptionsDiagnostics();
|
|
}
|
|
|
|
getSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[];
|
|
getSyntacticClassifications(fileName: string, span: ts.TextSpan, format: ts.SemanticClassificationFormat): ts.ClassifiedSpan[] | ts.ClassifiedSpan2020[];
|
|
getSyntacticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.ClassifiedSpan[] | ts.ClassifiedSpan2020[] {
|
|
return withPosition(
|
|
fileName, span.start,
|
|
() => this.inner.getSyntacticClassifications(fileName, span, format!),
|
|
() => [],
|
|
(fixup) => {
|
|
const cs = this.inner.getSyntacticClassifications(fileName, span, format!);
|
|
cs.forEach((c: ts.ClassifiedSpan | ts.ClassifiedSpan2020) => fixup.span(c.textSpan));
|
|
return cs;
|
|
});
|
|
}
|
|
|
|
getSemanticClassifications(fileName: string, span: ts.TextSpan): ts.ClassifiedSpan[];
|
|
getSemanticClassifications(fileName: string, span: ts.TextSpan, format: ts.SemanticClassificationFormat): ts.ClassifiedSpan[] | ts.ClassifiedSpan2020[];
|
|
getSemanticClassifications(fileName: any, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.ClassifiedSpan[] | ts.ClassifiedSpan2020[] {
|
|
return withPosition(
|
|
fileName, span.start,
|
|
() => this.inner.getSemanticClassifications(fileName, span, format!),
|
|
() => [],
|
|
(fixup) => {
|
|
const cs = this.inner.getSemanticClassifications(fileName, span, format!);
|
|
cs.forEach((c: ts.ClassifiedSpan | ts.ClassifiedSpan2020) => fixup.span(c.textSpan));
|
|
return cs;
|
|
});
|
|
}
|
|
|
|
getEncodedSyntacticClassifications(fileName: string, span: ts.TextSpan): ts.Classifications {
|
|
return withPosition(
|
|
fileName, span.start,
|
|
() => this.inner.getEncodedSyntacticClassifications(fileName, span),
|
|
() => ({ spans: [], endOfLineState: ts.EndOfLineState.None }),
|
|
(fixup) => {
|
|
const cs = this.inner.getEncodedSyntacticClassifications(fileName, span);
|
|
for (let i = 0; i < cs.spans.length; i += 3) {
|
|
const newStart = fixup.loc(cs.spans[i]);
|
|
if (newStart === void 0) {
|
|
cs.spans.splice(i, 3);
|
|
} else {
|
|
cs.spans[i] = newStart;
|
|
}
|
|
}
|
|
return cs;
|
|
});
|
|
}
|
|
|
|
getEncodedSemanticClassifications(fileName: string, span: ts.TextSpan, format?: ts.SemanticClassificationFormat): ts.Classifications {
|
|
return withPosition(
|
|
fileName, span.start,
|
|
() => this.inner.getEncodedSemanticClassifications(fileName, span, format),
|
|
() => ({ spans: [], endOfLineState: ts.EndOfLineState.None }),
|
|
(fixup) => {
|
|
const cs = this.inner.getEncodedSemanticClassifications(fileName, span, format);
|
|
for (let i = 0; i < cs.spans.length; i += 3) {
|
|
const newStart = fixup.loc(cs.spans[i]);
|
|
if (newStart === void 0) {
|
|
cs.spans.splice(i, 3);
|
|
} else {
|
|
cs.spans[i] = newStart;
|
|
}
|
|
}
|
|
return cs;
|
|
});
|
|
}
|
|
|
|
getCompletionsAtPosition(fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions | undefined): ts.WithMetadata<ts.CompletionInfo> | undefined {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.getCompletionsAtPosition(fileName, position, options),
|
|
() => void 0,
|
|
(fixup) => {
|
|
const cs = this.inner.getCompletionsAtPosition(fileName, fixup.targetStart, options);
|
|
if (cs !== void 0) {
|
|
fixup.span(cs.optionalReplacementSpan);
|
|
cs.entries.forEach(c => fixup.span(c.replacementSpan));
|
|
}
|
|
return cs;
|
|
});
|
|
}
|
|
|
|
getCompletionEntryDetails(fileName: string,
|
|
position: number,
|
|
entryName: string,
|
|
formatOptions: ts.FormatCodeOptions | ts.FormatCodeSettings | undefined,
|
|
source: string | undefined,
|
|
preferences: ts.UserPreferences | undefined,
|
|
data: ts.CompletionEntryData | undefined): ts.CompletionEntryDetails | undefined {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.getCompletionEntryDetails(fileName, position, entryName, formatOptions, source, preferences, data),
|
|
() => void 0,
|
|
(fixup) => {
|
|
const d = this.inner.getCompletionEntryDetails(fileName, fixup.targetStart, entryName, formatOptions, source, preferences, data);
|
|
if (d !== void 0) {
|
|
d.codeActions?.forEach(a =>
|
|
a.changes.forEach(c =>
|
|
c.textChanges.forEach(c =>
|
|
fixup.span(c.span))));
|
|
}
|
|
return d;
|
|
});
|
|
}
|
|
|
|
getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): ts.Symbol | undefined {
|
|
// TODO: hmm. Is this acceptable?
|
|
return void 0;
|
|
}
|
|
|
|
getQuickInfoAtPosition(fileName: string, position: number): ts.QuickInfo | undefined {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.getQuickInfoAtPosition(fileName, position),
|
|
() => void 0,
|
|
(fixup) => {
|
|
const qi = this.inner.getQuickInfoAtPosition(fileName, fixup.targetStart);
|
|
if (qi !== void 0) fixup.span(qi.textSpan);
|
|
return qi;
|
|
});
|
|
}
|
|
|
|
getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): ts.TextSpan | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems | undefined {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.getSignatureHelpItems(fileName, position, options),
|
|
() => void 0,
|
|
(fixup) => {
|
|
const items = this.inner.getSignatureHelpItems(fileName, fixup.targetStart, options);
|
|
if (items !== void 0) fixup.span(items.applicableSpan);
|
|
return items;
|
|
});
|
|
}
|
|
|
|
getRenameInfo(fileName: string, position: number, options?: ts.RenameInfoOptions): ts.RenameInfo {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.getRenameInfo(fileName, position, options),
|
|
() => ({ canRename: false, localizedErrorMessage: 'Identifier not present in expanded source code' }),
|
|
(fixup) => {
|
|
const ri = this.inner.getRenameInfo(fileName, fixup.targetStart, options);
|
|
if (ri.canRename) {
|
|
fixup.span(ri.triggerSpan);
|
|
}
|
|
return ri;
|
|
});
|
|
}
|
|
|
|
findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): readonly ts.RenameLocation[] | undefined {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename),
|
|
() => void 0,
|
|
(fixup) => {
|
|
const locs = this.inner.findRenameLocations(fileName, fixup.targetStart, findInStrings, findInComments, providePrefixAndSuffixTextForRename);
|
|
if (locs !== void 0) locs.forEach(fixupDocumentSpan);
|
|
return locs;
|
|
});
|
|
}
|
|
|
|
getSmartSelectionRange(fileName: string, position: number): ts.SelectionRange {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getDefinitionAtPosition(fileName: string, position: number): readonly ts.DefinitionInfo[] | undefined {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.getDefinitionAtPosition(fileName, position),
|
|
() => undefined,
|
|
(fixup) => {
|
|
const dis = this.inner.getDefinitionAtPosition(fileName, fixup.targetStart);
|
|
if (dis !== void 0) dis.forEach(fixupDocumentSpan);
|
|
return dis;
|
|
});
|
|
}
|
|
|
|
getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan | undefined {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.getDefinitionAndBoundSpan(fileName, position),
|
|
() => undefined,
|
|
(fixup) => {
|
|
const d = this.inner.getDefinitionAndBoundSpan(fileName, fixup.targetStart);
|
|
if (d !== void 0) {
|
|
d.definitions?.forEach(fixupDocumentSpan);
|
|
fixup.span(d.textSpan);
|
|
}
|
|
return d;
|
|
});
|
|
}
|
|
|
|
getTypeDefinitionAtPosition(fileName: string, position: number): readonly ts.DefinitionInfo[] | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getImplementationAtPosition(fileName: string, position: number): readonly ts.ImplementationLocation[] | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
findReferences(fileName: string, position: number): ts.ReferencedSymbol[] | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] | undefined {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.getDocumentHighlights(fileName, position, filesToSearch),
|
|
() => [],
|
|
fixup => {
|
|
const dhs = this.inner.getDocumentHighlights(fileName, fixup.targetStart, filesToSearch);
|
|
if (dhs === void 0) return dhs;
|
|
dhs.forEach(dh => dh.highlightSpans.forEach((s: tslib.HighlightSpan) => {
|
|
fixup.span(s.textSpan);
|
|
fixup.span(s.contextSpan);
|
|
}));
|
|
return dhs;
|
|
});
|
|
}
|
|
|
|
getOccurrencesAtPosition(fileName: string, position: number): readonly ts.ReferenceEntry[] | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean): ts.NavigateToItem[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getNavigationBarItems(fileName: string): ts.NavigationBarItem[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getNavigationTree(fileName: string): ts.NavigationTree {
|
|
const t = this.inner.getNavigationTree(fileName);
|
|
return withFileName(fileName, () => t, (fixup) => {
|
|
function fixupNavigationTree(t: ts.NavigationTree) {
|
|
fixup.span(t.nameSpan);
|
|
t.spans.forEach(s => fixup.span(s));
|
|
t.childItems?.forEach(fixupNavigationTree);
|
|
}
|
|
fixupNavigationTree(t);
|
|
return t;
|
|
});
|
|
}
|
|
|
|
prepareCallHierarchy(fileName: string, position: number): ts.CallHierarchyItem | ts.CallHierarchyItem[] | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
provideCallHierarchyIncomingCalls(fileName: string, position: number): ts.CallHierarchyIncomingCall[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
provideCallHierarchyOutgoingCalls(fileName: string, position: number): ts.CallHierarchyOutgoingCall[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getOutliningSpans(fileName: string): ts.OutliningSpan[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getTodoComments(fileName: string, descriptors: ts.TodoCommentDescriptor[]): ts.TodoComment[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getBraceMatchingAtPosition(fileName: string, position: number): ts.TextSpan[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getIndentationAtPosition(fileName: string, position: number, options: ts.EditorOptions | ts.EditorSettings): number {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.TextChange[] {
|
|
return withPositions(
|
|
fileName, [start, end],
|
|
() => this.inner.getFormattingEditsForRange(fileName, start, end, options),
|
|
() => [],
|
|
([fixStart, fixEnd]) => {
|
|
const edits = this.inner.getFormattingEditsForRange(fileName, fixStart.targetStart, fixEnd.targetEnd, options);
|
|
edits.forEach(e => fixStart.span(e.span));
|
|
return edits;
|
|
});
|
|
}
|
|
|
|
getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.TextChange[] {
|
|
const edits = this.inner.getFormattingEditsForDocument(fileName, options);
|
|
withFileName(fileName, () => void 0, (fixup) => edits.forEach(e => fixup.span(e.span)));
|
|
return edits;
|
|
}
|
|
|
|
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.TextChange[] {
|
|
return withPosition(
|
|
fileName, position,
|
|
() => this.inner.getFormattingEditsAfterKeystroke(fileName, position, key, options),
|
|
() => [],
|
|
(fixup) => {
|
|
const edits = this.inner.getFormattingEditsAfterKeystroke(fileName, fixup.targetStart, key, options);
|
|
edits.forEach(e => fixup.span(e.span));
|
|
return edits;
|
|
});
|
|
}
|
|
|
|
getDocCommentTemplateAtPosition(fileName: string, position: number): ts.TextInsertion | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getJsxClosingTagAtPosition(fileName: string, position: number): ts.JsxClosingTagInfo | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences): readonly ts.CodeFixAction[] {
|
|
return withPositions(
|
|
fileName, [start, end],
|
|
() => this.inner.getCodeFixesAtPosition(fileName,
|
|
start,
|
|
end,
|
|
errorCodes,
|
|
formatOptions,
|
|
preferences),
|
|
() => [],
|
|
([fixStart, fixEnd]) => {
|
|
const fixes = this.inner.getCodeFixesAtPosition(fileName,
|
|
fixStart.targetStart,
|
|
fixEnd.targetEnd,
|
|
errorCodes,
|
|
formatOptions,
|
|
preferences);
|
|
fixes.forEach(f =>
|
|
f.changes.forEach(change =>
|
|
change.textChanges.forEach(c =>
|
|
fixStart.span(c.span))));
|
|
return fixes;
|
|
});
|
|
}
|
|
|
|
getCombinedCodeFix(scope: ts.CombinedCodeFixScope, fixId: {}, formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences): ts.CombinedCodeActions {
|
|
const actions = this.inner.getCombinedCodeFix(scope, fixId, formatOptions, preferences);
|
|
actions.changes.forEach(change => {
|
|
const info = getInfo(change.fileName);
|
|
if (info !== void 0) {
|
|
const fixup = new Fixup(info);
|
|
change.textChanges.forEach(c => fixup.span(c.span));
|
|
}
|
|
});
|
|
return actions;
|
|
}
|
|
|
|
applyCodeActionCommand(action: ts.InstallPackageAction, formatSettings?: ts.FormatCodeSettings): Promise<ts.ApplyCodeActionCommandResult>;
|
|
applyCodeActionCommand(action: ts.InstallPackageAction[], formatSettings?: ts.FormatCodeSettings): Promise<ts.ApplyCodeActionCommandResult[]>;
|
|
applyCodeActionCommand(action: ts.InstallPackageAction | ts.InstallPackageAction[], formatSettings?: ts.FormatCodeSettings): Promise<ts.ApplyCodeActionCommandResult | ts.ApplyCodeActionCommandResult[]>;
|
|
applyCodeActionCommand(fileName: string, action: ts.InstallPackageAction): Promise<ts.ApplyCodeActionCommandResult>;
|
|
applyCodeActionCommand(fileName: string, action: ts.InstallPackageAction[]): Promise<ts.ApplyCodeActionCommandResult[]>;
|
|
applyCodeActionCommand(fileName: string, action: ts.InstallPackageAction | ts.InstallPackageAction[]): Promise<ts.ApplyCodeActionCommandResult | ts.ApplyCodeActionCommandResult[]>;
|
|
applyCodeActionCommand(fileName: any, action?: any): any {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getApplicableRefactors(fileName: string, positionOrRange: number | ts.TextRange, preferences: ts.UserPreferences | undefined, triggerReason?: ts.RefactorTriggerReason): ts.ApplicableRefactorInfo[] {
|
|
return this.inner.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason);
|
|
}
|
|
|
|
getEditsForRefactor(fileName: string, formatOptions: ts.FormatCodeSettings, positionOrRange: number | ts.TextRange, refactorName: string, actionName: string, preferences: ts.UserPreferences | undefined): ts.RefactorEditInfo | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
organizeImports(scope: ts.CombinedCodeFixScope, formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences | undefined): readonly ts.FileTextChanges[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences | undefined): readonly ts.FileTextChanges[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean): ts.EmitOutput {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getProgram(): ts.Program | undefined {
|
|
return this.inner.getProgram();
|
|
}
|
|
|
|
toggleLineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
toggleMultilineComment(fileName: string, textRange: ts.TextRange): ts.TextChange[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
commentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
uncommentSelection(fileName: string, textRange: ts.TextRange): ts.TextChange[] {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
dispose(): void {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getNonBoundSourceFile(fileName: string): ts.SourceFile {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
getAutoImportProvider(): ts.Program | undefined {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
|
|
toLineColumnOffset(fileName: string, position: number): ts.LineAndCharacter {
|
|
function search(t: string | undefined, position: number): ts.LineAndCharacter {
|
|
if (t === void 0) return { line: 0, character: 0 };
|
|
const p = Syntax.startPos(fileName);
|
|
for (let i = 0; i < position; i++) Syntax.advancePos(p, t[i]);
|
|
return { line: p.line - 1, character: p.column };
|
|
}
|
|
return withFileName(
|
|
fileName,
|
|
() => (this.inner.toLineColumnOffset?.(fileName, position) ??
|
|
search(this.inner.getProgram()?.getSourceFile(fileName)?.text, position)),
|
|
(fixup) =>
|
|
search(fixup.info.originalSource, position));
|
|
}
|
|
|
|
// getSourceMapper(): ts.SourceMapper {
|
|
// throw new Error('Method not implemented.');
|
|
// }
|
|
|
|
clearSourceMapperCache(): void {
|
|
throw new Error('Method not implemented.');
|
|
}
|
|
}
|
|
|
|
function finalSlash(s: string): string {
|
|
if (s[s.length - 1] !== '/') s = s + '/';
|
|
return s;
|
|
}
|
|
|
|
class SyndicatePlugin implements ts.server.PluginModule {
|
|
create(createInfo: ts.server.PluginCreateInfo): ts.LanguageService {
|
|
const project = createInfo.project;
|
|
|
|
const options = project.getCompilerOptions();
|
|
if (options.rootDir !== void 0) {
|
|
syndicateRootDirs.add(finalSlash(options.rootDir));
|
|
}
|
|
if (options.rootDirs !== void 0) {
|
|
options.rootDirs.forEach(d => syndicateRootDirs.add(finalSlash(d)));
|
|
}
|
|
if (options.rootDir === void 0 && options.rootDirs === void 0) {
|
|
syndicateRootDirs.add(finalSlash(path.resolve('.')));
|
|
}
|
|
|
|
// // 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);
|
|
}
|
|
}
|
|
|
|
return new SyndicatePlugin();
|
|
|
|
};
|
|
export = boot;
|