diff --git a/packages/website/src/components/editor/createProvideTwoslashInlay.ts b/packages/website/src/components/editor/createProvideTwoslashInlay.ts new file mode 100644 index 000000000000..4a2c6d2653a3 --- /dev/null +++ b/packages/website/src/components/editor/createProvideTwoslashInlay.ts @@ -0,0 +1,98 @@ +// The following code is adapted from the code in microsoft/TypeScript-Website. +// Source: https://github.com/microsoft/TypeScript-Website/blob/c8b2ea8c8fc216c163fe293650b2666c8563a67d/packages/playground/src/twoslashInlays.ts +// License: https://github.com/microsoft/TypeScript-Website/blob/c8b2ea8c8fc216c163fe293650b2666c8563a67d/LICENSE-CODE + +import type Monaco from 'monaco-editor'; +import type * as ts from 'typescript'; + +import type { SandboxInstance } from './useSandboxServices'; + +function findTwoshashQueries(code: string): RegExpExecArray[] { + let match: RegExpExecArray | null = null; + const matches: RegExpExecArray[] = []; + // RegExp that matches '^//?$' + const twoslashQueryRegex = /^(\s*\/\/\s*\^\?)\s*$/gm; + while ((match = twoslashQueryRegex.exec(code))) { + matches.push(match); + } + return matches; +} + +export function createTwoslashInlayProvider( + sandbox: SandboxInstance, +): Monaco.languages.InlayHintsProvider { + return { + provideInlayHints: async ( + model, + _, + cancel, + ): Promise => { + const worker = await sandbox.getWorkerProcess(); + if (model.isDisposed() || cancel.isCancellationRequested) { + return { + hints: [], + dispose(): void { + /* nop */ + }, + }; + } + + const queryMatches = findTwoshashQueries(model.getValue()); + + const results: Monaco.languages.InlayHint[] = []; + + for (const result of await Promise.all( + queryMatches.map(q => resolveInlayHint(q)), + )) { + if (result) { + results.push(result); + } + } + + return { + hints: results, + dispose(): void { + /* nop */ + }, + }; + + async function resolveInlayHint( + queryMatch: RegExpExecArray, + ): Promise { + const end = queryMatch.index + queryMatch[1].length - 1; + const endPos = model.getPositionAt(end); + const inspectionPos = new sandbox.monaco.Position( + endPos.lineNumber - 1, + endPos.column, + ); + const inspectionOff = model.getOffsetAt(inspectionPos); + + const hint = await (worker.getQuickInfoAtPosition( + 'file://' + model.uri.path, + inspectionOff, + ) as Promise); + if (!hint?.displayParts) { + return; + } + + let text = hint.displayParts + .map(d => d.text) + .join('') + .replace(/\r?\n\s*/g, ' '); + if (text.length > 120) { + text = text.slice(0, 119) + '...'; + } + + return { + kind: sandbox.monaco.languages.InlayHintKind.Type, + position: new sandbox.monaco.Position( + endPos.lineNumber, + endPos.column + 1, + ), + label: text, + paddingLeft: true, + }; + } + }, + }; +} diff --git a/packages/website/src/components/editor/useSandboxServices.ts b/packages/website/src/components/editor/useSandboxServices.ts index 997cc1cc6058..bc8d1ac0582d 100644 --- a/packages/website/src/components/editor/useSandboxServices.ts +++ b/packages/website/src/components/editor/useSandboxServices.ts @@ -10,6 +10,7 @@ import { createFileSystem } from '../linter/bridge'; import { type CreateLinter, createLinter } from '../linter/createLinter'; import type { PlaygroundSystem } from '../linter/types'; import type { RuleDetails } from '../types'; +import { createTwoslashInlayProvider } from './createProvideTwoslashInlay'; import { editorEmbedId } from './EditorEmbed'; import { sandboxSingleton } from './loadSandbox'; import type { CommonEditorProps } from './types'; @@ -71,6 +72,13 @@ export const useSandboxServices = ( colorMode === 'dark' ? 'vs-dark' : 'vs-light', ); + // registerInlayHintsProvider was added in TS 4.4 and isn't in TS <= 4.3. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + sandboxInstance.monaco.languages.registerInlayHintsProvider?.( + sandboxInstance.language, + createTwoslashInlayProvider(sandboxInstance), + ); + const system = createFileSystem(props, sandboxInstance.tsvfs); const worker = await sandboxInstance.getWorkerProcess();