/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones 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; sourceToTargetMap: Syntax.SpanIndex; } const syndicateInfo: Map = new Map(); const syndicateRootDirs: Set = 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(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(ds: T[]): T[] { const vs: T[] = []; ds.forEach(d => this.diagnostic(d, vs)); return vs; } } class PositionFixup extends Fixup { readonly target: Syntax.SpanResult; constructor(info: SyndicateInfo, target: Syntax.SpanResult) { 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(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(fileName: string, positions: Array, kNoInfo: () => T, kNoPosition: () => T, k: (f: Array) => 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(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, sourceToTargetMap: Syntax.SpanIndex, 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 | 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; applyCodeActionCommand(action: ts.InstallPackageAction[], formatSettings?: ts.FormatCodeSettings): Promise; applyCodeActionCommand(action: ts.InstallPackageAction | ts.InstallPackageAction[], formatSettings?: ts.FormatCodeSettings): Promise; applyCodeActionCommand(fileName: string, action: ts.InstallPackageAction): Promise; applyCodeActionCommand(fileName: string, action: ts.InstallPackageAction[]): Promise; applyCodeActionCommand(fileName: string, action: ts.InstallPackageAction | ts.InstallPackageAction[]): Promise; 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 = // (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;