Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Export `resolveCodeFix` function to allow resolving a `CodeFix` into `CodeFixEdit[]` without the LSP layer.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/playground"
---

Add codefix support in the playground. Quick fixes are now surfaced as Monaco code actions when the cursor is on a diagnostic.
2 changes: 1 addition & 1 deletion packages/compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export const $decorators = {
},
};

export { applyCodeFix, applyCodeFixes } from "./core/code-fixes.js";
export { applyCodeFix, applyCodeFixes, resolveCodeFix } from "./core/code-fixes.js";
export { createAddDecoratorCodeFix } from "./core/compiler-code-fixes/create-add-decorator/create-add-decorator.codefix.js";
export {
createSuppressCodeFix,
Expand Down
6 changes: 5 additions & 1 deletion packages/playground/src/react/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { CompletionItemTag } from "vscode-languageserver";
import { resolveVirtualPath } from "../browser-host.js";
import { EditorCommandBar } from "../editor-command-bar/editor-command-bar.js";
import { getMonacoRange } from "../services.js";
import { getMonacoRange, updateDiagnosticsForCodeFixes } from "../services.js";
import type { BrowserHost, PlaygroundSample } from "../types.js";
import { PlaygroundContextProvider } from "./context/playground-context.js";
import { debugGlobals, printDebugInfo } from "./debug.js";
Expand Down Expand Up @@ -224,12 +224,16 @@ export const Playground: FunctionComponent<PlaygroundProps> = (props) => {
tags: diag.code === "deprecated" ? [CompletionItemTag.Deprecated] : undefined,
}));

// Update code action provider with current diagnostics (for codefix support).
updateDiagnosticsForCodeFixes(typespecCompiler, state.program.diagnostics);

// Set the program on the window.
debugGlobals().program = state.program;
debugGlobals().$$ = $(state.program);

editor.setModelMarkers(typespecModel, "owner", markers ?? []);
} else {
updateDiagnosticsForCodeFixes(typespecCompiler, []);
editor.setModelMarkers(typespecModel, "owner", []);
}
}, [host, selectedEmitter, compilerOptions, typespecModel]);
Expand Down
108 changes: 97 additions & 11 deletions packages/playground/src/services.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
TypeSpecLanguageConfiguration,
type Diagnostic,
type DiagnosticTarget,
type NoTarget,
type ServerHost,
Expand All @@ -12,6 +13,24 @@ import { LspToMonaco } from "./lsp/lsp-to-monaco.js";
import { MonacoToLsp } from "./lsp/monaco-to-lsp.js";
import type { BrowserHost } from "./types.js";

// Module-level store for diagnostics used by the code action provider.
// Updated on each playground compilation via updateDiagnosticsForCodeFixes().
let _currentDiagnostics: readonly Diagnostic[] = [];
let _currentCompiler: typeof import("@typespec/compiler") | undefined;

/**
* Update the current diagnostics so the Monaco code action provider can
* surface codefixes for the playground's own compilation results.
* Call this after every compilation.
*/
export function updateDiagnosticsForCodeFixes(
compiler: typeof import("@typespec/compiler"),
diagnostics: readonly Diagnostic[],
) {
_currentDiagnostics = diagnostics;
_currentCompiler = compiler;
}

function getIndentAction(
value: "none" | "indent" | "indentOutdent" | "outdent",
): monaco.languages.IndentAction {
Expand Down Expand Up @@ -306,17 +325,71 @@ export async function registerMonacoLanguage(host: BrowserHost) {
},
});

// This doesn't actually work because the lsp is not aware of the diagnostics here as we make our own compilation in the playground.
// monaco.languages.registerCodeActionProvider("typespec", {
// async provideCodeActions(model, range, context, token) {
// const result = await serverLib.getCodeActions({
// range: MonacoToLsp.range(range),
// context: MonacoToLsp.codeActionContext(context),
// textDocument: textDocumentForModel(model),
// });
// return { actions: result.map(LspToMonaco.codeAction), dispose: () => {} };
// },
// });
// Register a code action provider that uses the playground's own compilation
// diagnostics (which include codefixes) rather than the LSP server diagnostics.
monaco.languages.registerCodeActionProvider("typespec", {
async provideCodeActions(model, range) {
const compiler = _currentCompiler;
if (!compiler) return { actions: [], dispose: () => {} };

const actions: monaco.languages.CodeAction[] = [];
for (const diag of _currentDiagnostics) {
if (!diag.codefixes?.length) continue;
const loc = compiler.getSourceLocation(diag.target, { locateId: true });
if (!loc || loc.file.path !== "/test/main.tsp") continue;
const monacoRange = getMonacoRange(compiler, diag.target);
if (!monacoRangesOverlap(monacoRange, range)) continue;

for (const fix of diag.codefixes) {
const edits = await compiler.resolveCodeFix(fix);
const workspaceEdits: monaco.languages.IWorkspaceTextEdit[] = edits
.filter((edit) => edit.file.path === "/test/main.tsp")
.map((edit) => {
const start = edit.file.getLineAndCharacterOfPosition(edit.pos);
if (edit.kind === "insert-text") {
return {
resource: model.uri,
textEdit: {
range: {
startLineNumber: start.line + 1,
startColumn: start.character + 1,
endLineNumber: start.line + 1,
endColumn: start.character + 1,
},
text: edit.text,
},
versionId: undefined,
};
} else {
const end = edit.file.getLineAndCharacterOfPosition(edit.end);
return {
resource: model.uri,
textEdit: {
range: {
startLineNumber: start.line + 1,
startColumn: start.character + 1,
endLineNumber: end.line + 1,
endColumn: end.character + 1,
},
text: edit.text,
},
versionId: undefined,
};
}
});

if (workspaceEdits.length > 0) {
actions.push({
title: fix.label,
kind: "quickfix",
edit: { edits: workspaceEdits },
});
}
}
}
return { actions, dispose: () => {} };
},
});

monaco.editor.defineTheme("typespec", {
base: "vs",
Expand Down Expand Up @@ -395,3 +468,16 @@ export function getMonacoRange(
endColumn: end.character + 1,
};
}

function monacoRangesOverlap(a: monaco.IRange, b: monaco.IRange): boolean {
if (a.endLineNumber < b.startLineNumber || b.endLineNumber < a.startLineNumber) {
return false;
}
if (a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) {
return false;
}
if (b.endLineNumber === a.startLineNumber && b.endColumn < a.startColumn) {
return false;
}
return true;
}
Loading