diff --git a/playground/package-lock.json b/playground/package-lock.json
index 3de5b851bf..81260beb3a 100644
--- a/playground/package-lock.json
+++ b/playground/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"workspaces": [
"ty",
+ "ty-embed",
"ruff",
"shared"
],
@@ -6081,6 +6082,10 @@
"resolved": "ty/ty_wasm",
"link": true
},
+ "node_modules/ty-embed": {
+ "resolved": "ty-embed",
+ "link": true
+ },
"node_modules/ty-playground": {
"resolved": "ty",
"link": true
@@ -6608,6 +6613,22 @@
"vite-plugin-static-copy": "^3.0.0"
}
},
+ "ty-embed": {
+ "version": "0.0.0",
+ "dependencies": {
+ "monaco-editor": "^0.54.0",
+ "ty_wasm": "file:ty_wasm"
+ },
+ "devDependencies": {
+ "typescript": "^5.8.2",
+ "vite": "^7.0.0"
+ }
+ },
+ "ty-embed/ty_wasm": {
+ "version": "0.0.0",
+ "extraneous": true,
+ "license": "MIT"
+ },
"ty/ty_wasm": {
"version": "0.0.0",
"license": "MIT"
diff --git a/playground/package.json b/playground/package.json
index 771b356ae0..e431720d7f 100644
--- a/playground/package.json
+++ b/playground/package.json
@@ -9,11 +9,12 @@
"dev:build": "npm run dev:build --workspace ty-playground && npm run dev:build --workspace ruff-playground",
"fmt": "prettier --cache -w .",
"fmt:check": "prettier --cache --check .",
- "lint": "eslint --cache --ext .ts,.tsx ruff/src ty/src",
+ "lint": "eslint --cache --ext .ts,.tsx ruff/src ty/src ty-embed/src",
"tsc": "tsc"
},
"workspaces": [
"ty",
+ "ty-embed",
"ruff",
"shared"
],
diff --git a/playground/ty-embed/.gitignore b/playground/ty-embed/.gitignore
new file mode 100644
index 0000000000..1892b4af1f
--- /dev/null
+++ b/playground/ty-embed/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+dist
+ty_wasm
+.DS_Store
+*.log
diff --git a/playground/ty-embed/README.md b/playground/ty-embed/README.md
new file mode 100644
index 0000000000..689af81ef4
--- /dev/null
+++ b/playground/ty-embed/README.md
@@ -0,0 +1,3 @@
+# ty Embeddable Editor
+
+A simplified, embeddable version of the ty playground that can be used in documentation pages. This allows you to create multiple interactive Python type-checking editors on a single webpage.
diff --git a/playground/ty-embed/example-dev.html b/playground/ty-embed/example-dev.html
new file mode 100644
index 0000000000..a0598b25e2
--- /dev/null
+++ b/playground/ty-embed/example-dev.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+ ty Type Checker - Interactive Example
+
+
+
+
+
+ ty Type Checker
+
+
+ This interactive editor provides real-time type checking.
+ Hover over symbols to see type information, Ctrl+click to jump to definitions,
+ and click on errors to see quick fix suggestions.
+
+
+ TypedDict Keys
+
+ The code below has a typo in the dictionary key. Click on the error
+ and use the quick fix (lightbulb icon or Ctrl+.) to automatically fix it:
+
+
+
+
+
+
+
diff --git a/playground/ty-embed/example.html b/playground/ty-embed/example.html
new file mode 100644
index 0000000000..a082ee6946
--- /dev/null
+++ b/playground/ty-embed/example.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+ ty Type Checker - Interactive Example
+
+
+
+
+
+
+ ty Type Checker
+
+
+ This interactive editor provides real-time type checking.
+ Hover over symbols to see type information, Ctrl+click to jump to definitions,
+ and click on errors to see quick fix suggestions.
+
+
+ TypedDict Keys
+
+ The code below has a typo in the dictionary key. Click on the error
+ and use the quick fix (lightbulb icon or Ctrl+.) to automatically fix it:
+
+
+
+
+
+
+
+
diff --git a/playground/ty-embed/package.json b/playground/ty-embed/package.json
new file mode 100644
index 0000000000..e7d38bfed0
--- /dev/null
+++ b/playground/ty-embed/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "ty-embed",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "prebuild": "npm run build:wasm",
+ "build": "vite build",
+ "build:wasm": "wasm-pack build ../../crates/ty_wasm --target web --out-dir ../../playground/ty-embed/ty_wasm",
+ "dev:wasm": "wasm-pack build ../../crates/ty_wasm --dev --target web --out-dir ../../playground/ty-embed/ty_wasm",
+ "predev:build": "npm run dev:wasm",
+ "dev:build": "vite build",
+ "prestart": "npm run dev:wasm",
+ "start": "vite"
+ },
+ "dependencies": {
+ "monaco-editor": "^0.54.0",
+ "ty_wasm": "file:ty_wasm"
+ },
+ "devDependencies": {
+ "typescript": "^5.8.2",
+ "vite": "^7.0.0",
+ "vite-plugin-static-copy": "^2.2.0"
+ }
+}
diff --git a/playground/ty-embed/src/EmbeddableEditor.ts b/playground/ty-embed/src/EmbeddableEditor.ts
new file mode 100644
index 0000000000..a8053360cd
--- /dev/null
+++ b/playground/ty-embed/src/EmbeddableEditor.ts
@@ -0,0 +1,843 @@
+import * as monaco from "monaco-editor";
+import {
+ FileHandle,
+ PositionEncoding,
+ Workspace,
+ Range as TyRange,
+ Severity,
+ Position as TyPosition,
+ CompletionKind,
+ LocationLink,
+ TextEdit,
+ InlayHintKind,
+ Diagnostic as TyDiagnostic,
+} from "ty_wasm";
+
+// Ayu theme colors from the ty playground
+const RADIATE = "#d7ff64";
+const ROCK = "#78876e";
+const COSMIC = "#de5fe9";
+const SUN = "#ffac2f";
+const ELECTRON = "#46ebe1";
+const CONSTELLATION = "#5f6de9";
+const STARLIGHT = "#f4f4f1";
+const PROTON = "#f6afbc";
+const SUPERNOVA = "#f1aff6";
+const ASTEROID = "#e3cee3";
+
+let themesInitialized = false;
+
+function defineAyuThemes() {
+ if (themesInitialized) return;
+ themesInitialized = true;
+
+ // Ayu Light theme
+ monaco.editor.defineTheme("Ayu-Light", {
+ inherit: false,
+ base: "vs",
+ colors: {
+ "editor.background": "#f8f9fa",
+ "editor.foreground": "#5c6166",
+ "editorLineNumber.foreground": "#8a919966",
+ "editorLineNumber.activeForeground": "#8a9199cc",
+ "editorCursor.foreground": "#ffaa33",
+ "editor.selectionBackground": "#035bd626",
+ "editor.lineHighlightBackground": "#8a91991a",
+ "editorIndentGuide.background": "#8a91992e",
+ "editorIndentGuide.activeBackground": "#8a919959",
+ "editorError.foreground": "#e65050",
+ "editorWarning.foreground": "#ffaa33",
+ "editorWidget.background": "#f3f4f5",
+ "editorWidget.border": "#6b7d8f1f",
+ "editorHoverWidget.background": "#f3f4f5",
+ "editorHoverWidget.border": "#6b7d8f1f",
+ "editorSuggestWidget.background": "#f3f4f5",
+ "editorSuggestWidget.border": "#6b7d8f1f",
+ "editorSuggestWidget.highlightForeground": "#ffaa33",
+ "editorSuggestWidget.selectedBackground": "#56728f1f",
+ },
+ rules: [
+ { fontStyle: "italic", foreground: "#787b8099", token: "comment" },
+ { foreground: COSMIC, token: "keyword" },
+ { foreground: COSMIC, token: "builtinConstant" },
+ { foreground: CONSTELLATION, token: "number" },
+ { foreground: ROCK, token: "tag" },
+ { foreground: ROCK, token: "string" },
+ { foreground: SUN, token: "method" },
+ { foreground: SUN, token: "function" },
+ { foreground: SUN, token: "decorator" },
+ ],
+ encodedTokensColors: [],
+ });
+
+ // Ayu Dark theme
+ monaco.editor.defineTheme("Ayu-Dark", {
+ inherit: false,
+ base: "vs-dark",
+ colors: {
+ "editor.background": "#0b0e14",
+ "editor.foreground": "#bfbdb6",
+ "editorLineNumber.foreground": "#6c738099",
+ "editorLineNumber.activeForeground": "#6c7380e6",
+ "editorCursor.foreground": "#e6b450",
+ "editor.selectionBackground": "#409fff4d",
+ "editor.lineHighlightBackground": "#131721",
+ "editorIndentGuide.background": "#6c738033",
+ "editorIndentGuide.activeBackground": "#6c738080",
+ "editorError.foreground": "#d95757",
+ "editorWarning.foreground": "#e6b450",
+ "editorWidget.background": "#0f131a",
+ "editorWidget.border": "#11151c",
+ "editorHoverWidget.background": "#0f131a",
+ "editorHoverWidget.border": "#11151c",
+ "editorSuggestWidget.background": "#0f131a",
+ "editorSuggestWidget.border": "#11151c",
+ "editorSuggestWidget.highlightForeground": "#e6b450",
+ "editorSuggestWidget.selectedBackground": "#47526640",
+ },
+ rules: [
+ { fontStyle: "italic", foreground: "#acb6bf8c", token: "comment" },
+ { foreground: ELECTRON, token: "string" },
+ { foreground: CONSTELLATION, token: "number" },
+ { foreground: STARLIGHT, token: "identifier" },
+ { foreground: RADIATE, token: "keyword" },
+ { foreground: RADIATE, token: "builtinConstant" },
+ { foreground: PROTON, token: "tag" },
+ { foreground: ASTEROID, token: "delimiter" },
+ { foreground: SUPERNOVA, token: "class" },
+ { foreground: STARLIGHT, token: "variable" },
+ { foreground: STARLIGHT, token: "parameter" },
+ { foreground: SUN, token: "method" },
+ { foreground: SUN, token: "function" },
+ { foreground: SUN, token: "decorator" },
+ ],
+ encodedTokensColors: [],
+ });
+}
+
+export interface EditorOptions {
+ initialCode?: string;
+ theme?: "light" | "dark";
+ fileName?: string;
+ settings?: Record;
+ height?: string;
+ showDiagnostics?: boolean;
+ id?: string;
+}
+
+interface Diagnostic {
+ id: string;
+ message: string;
+ severity: Severity;
+ range: TyRange | null;
+ raw: TyDiagnostic;
+}
+
+const DEFAULT_SETTINGS = {
+ environment: {
+ "python-version": "3.14",
+ },
+ rules: {
+ "undefined-reveal": "ignore",
+ },
+};
+
+export class EmbeddableEditor {
+ private container: HTMLElement;
+ private options: Required;
+ private editor: monaco.editor.IStandaloneCodeEditor | null = null;
+ private workspace: Workspace | null = null;
+ private fileHandle: FileHandle | null = null;
+ private languageServer: LanguageServer | null = null;
+ private diagnosticsContainer: HTMLElement | null = null;
+ private editorContainer: HTMLElement | null = null;
+ private errorContainer: HTMLElement | null = null;
+ private checkTimeoutId: number | null = null;
+
+ constructor(container: HTMLElement | string, options: EditorOptions) {
+ const element =
+ typeof container === "string"
+ ? document.querySelector(container)
+ : container;
+
+ if (!element) {
+ throw new Error(`Container not found: ${container}`);
+ }
+
+ this.container = element as HTMLElement;
+ this.options = {
+ initialCode: options.initialCode ?? "",
+ theme: options.theme ?? "light",
+ fileName: options.fileName ?? "main.py",
+ settings: options.settings ?? DEFAULT_SETTINGS,
+ height: options.height ?? "400px",
+ showDiagnostics: options.showDiagnostics ?? true,
+ id: options.id ?? `editor-${Date.now()}`,
+ };
+
+ this.init();
+ }
+
+ private async init() {
+ try {
+ // Create container structure
+ this.createContainerStructure();
+
+ // Initialize ty workspace
+ const ty = await import("ty_wasm");
+ await ty.default();
+
+ this.workspace = new Workspace("/", PositionEncoding.Utf16, {});
+ this.workspace.updateOptions(this.options.settings);
+ this.fileHandle = this.workspace.openFile(
+ this.options.fileName,
+ this.options.initialCode,
+ );
+
+ // Initialize Monaco editor
+ if (this.editorContainer) {
+ // Define Ayu themes before creating the editor
+ defineAyuThemes();
+
+ // Create model with URI matching the workspace file path
+ const modelUri = monaco.Uri.parse(this.options.fileName);
+ const model = monaco.editor.createModel(
+ this.options.initialCode,
+ "python",
+ modelUri,
+ );
+
+ this.editor = monaco.editor.create(this.editorContainer, {
+ model: model,
+ theme: this.options.theme === "light" ? "Ayu-Light" : "Ayu-Dark",
+ minimap: { enabled: false },
+ fontSize: 14,
+ scrollBeyondLastLine: false,
+ roundedSelection: false,
+ contextmenu: true,
+ automaticLayout: true,
+ fixedOverflowWidgets: true,
+ });
+
+ // Setup language server features
+ this.languageServer = new LanguageServer(
+ this.workspace,
+ this.fileHandle,
+ );
+
+ // Listen to content changes
+ this.editor.onDidChangeModelContent(() => {
+ this.onContentChange();
+ });
+
+ // Initial check
+ this.checkFile();
+ }
+ } catch (err) {
+ this.showError(this.formatError(err));
+ }
+ }
+
+ private createContainerStructure() {
+ const isLight = this.options.theme === "light";
+
+ this.container.style.height = this.options.height;
+ this.container.style.display = "flex";
+ this.container.style.flexDirection = "column";
+ this.container.style.border = isLight
+ ? "1px solid #6b7d8f1f"
+ : "1px solid #11151c";
+ this.container.style.borderRadius = "4px";
+ this.container.style.overflow = "hidden";
+ this.container.style.fontFamily =
+ 'Roboto Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
+
+ // Editor container
+ this.editorContainer = document.createElement("div");
+ this.editorContainer.style.height = this.options.showDiagnostics
+ ? `calc(${this.options.height} - 120px)`
+ : this.options.height;
+ this.container.appendChild(this.editorContainer);
+
+ // Diagnostics container
+ if (this.options.showDiagnostics) {
+ this.diagnosticsContainer = document.createElement("div");
+ this.diagnosticsContainer.style.height = "120px";
+ this.diagnosticsContainer.style.overflow = "auto";
+ this.diagnosticsContainer.style.borderTop = isLight
+ ? "1px solid #6b7d8f1f"
+ : "1px solid #11151c";
+ this.diagnosticsContainer.style.backgroundColor = isLight
+ ? "#f8f9fa"
+ : "#0b0e14";
+ this.diagnosticsContainer.style.color = isLight ? "#5c6166" : "#bfbdb6";
+ this.diagnosticsContainer.style.padding = "8px";
+ this.diagnosticsContainer.style.fontSize = "13px";
+ this.container.appendChild(this.diagnosticsContainer);
+ }
+ }
+
+ private onContentChange() {
+ // Debounce both workspace update and type checking
+ if (this.checkTimeoutId !== null) {
+ window.clearTimeout(this.checkTimeoutId);
+ }
+ this.checkTimeoutId = window.setTimeout(() => {
+ const content = this.editor?.getValue() ?? "";
+
+ if (this.workspace && this.fileHandle) {
+ try {
+ this.workspace.updateFile(this.fileHandle, content);
+ } catch (err) {
+ console.error("Error updating file:", err);
+ }
+ }
+
+ this.checkFile();
+ }, 150);
+ }
+
+ private checkFile() {
+ if (!this.workspace || !this.fileHandle || !this.editor) {
+ return;
+ }
+
+ try {
+ const diagnostics = this.workspace.checkFile(this.fileHandle);
+ const mapped: Diagnostic[] = diagnostics.map((diagnostic) => ({
+ id: diagnostic.id(),
+ message: diagnostic.message(),
+ severity: diagnostic.severity(),
+ range: diagnostic.toRange(this.workspace!) ?? null,
+ raw: diagnostic,
+ }));
+
+ this.updateDiagnostics(mapped);
+ this.hideError();
+ } catch (err) {
+ console.error("Error checking file:", err);
+ this.showError(this.formatError(err));
+ }
+ }
+
+ private updateDiagnostics(diagnostics: Diagnostic[]) {
+ // Update language server diagnostics for code actions
+ if (this.languageServer) {
+ this.languageServer.updateDiagnostics(diagnostics);
+ }
+
+ // Update Monaco markers
+ if (this.editor) {
+ const model = this.editor.getModel();
+ if (model) {
+ monaco.editor.setModelMarkers(
+ model,
+ "owner",
+ diagnostics.map((diagnostic) => {
+ const range = diagnostic.range;
+ return {
+ code: diagnostic.id,
+ startLineNumber: range?.start?.line ?? 1,
+ startColumn: range?.start?.column ?? 1,
+ endLineNumber: range?.end?.line ?? 1,
+ endColumn: range?.end?.column ?? 1,
+ message: diagnostic.message,
+ severity: this.mapSeverity(diagnostic.severity),
+ tags: [],
+ };
+ }),
+ );
+ }
+ }
+
+ // Update diagnostics panel
+ if (this.diagnosticsContainer) {
+ const isLight = this.options.theme === "light";
+ const mutedColor = isLight ? "#8a9199" : "#565b66";
+
+ this.diagnosticsContainer.innerHTML = "";
+
+ if (diagnostics.length > 0) {
+ const list = document.createElement("ul");
+ list.style.margin = "0";
+ list.style.padding = "0";
+ list.style.listStyle = "none";
+
+ diagnostics.forEach((diagnostic) => {
+ const item = this.createDiagnosticItem(diagnostic, mutedColor);
+ list.appendChild(item);
+ });
+ this.diagnosticsContainer.appendChild(list);
+ }
+ }
+ }
+
+ private createDiagnosticItem(
+ diagnostic: Diagnostic,
+ mutedColor: string,
+ ): HTMLElement {
+ const startLine = diagnostic.range?.start?.line ?? 1;
+ const startColumn = diagnostic.range?.start?.column ?? 1;
+ const isLight = this.options.theme === "light";
+
+ // Error: red, Warning: yellow/orange
+ const severityColor =
+ diagnostic.severity === Severity.Error
+ ? isLight
+ ? "#e65050"
+ : "#d95757"
+ : diagnostic.severity === Severity.Warning
+ ? isLight
+ ? "#f2ae49"
+ : "#e6b450"
+ : mutedColor;
+
+ const item = document.createElement("li");
+ item.style.marginBottom = "8px";
+ item.style.paddingLeft = "8px";
+ item.style.borderLeft = `3px solid ${severityColor}`;
+
+ const button = document.createElement("button");
+ button.style.all = "unset";
+ button.style.width = "100%";
+ button.style.textAlign = "left";
+ button.style.cursor = "pointer";
+ button.style.userSelect = "text";
+
+ button.innerHTML = `
+ ${diagnostic.id}
+ ${startLine}:${startColumn}
+ ${diagnostic.message}
+ `;
+
+ button.addEventListener("click", () => {
+ if (diagnostic.range && this.editor) {
+ const range = diagnostic.range;
+ this.editor.revealRange({
+ startLineNumber: range.start.line,
+ startColumn: range.start.column,
+ endLineNumber: range.end.line,
+ endColumn: range.end.column,
+ });
+ this.editor.setSelection({
+ startLineNumber: range.start.line,
+ startColumn: range.start.column,
+ endLineNumber: range.end.line,
+ endColumn: range.end.column,
+ });
+ this.editor.focus();
+ }
+ });
+
+ item.appendChild(button);
+ return item;
+ }
+
+ private showError(message: string) {
+ if (!this.errorContainer) {
+ this.errorContainer = document.createElement("div");
+ this.errorContainer.style.position = "absolute";
+ this.errorContainer.style.bottom = "10px";
+ this.errorContainer.style.left = "10px";
+ this.errorContainer.style.right = "10px";
+ this.errorContainer.style.padding = "12px";
+ this.errorContainer.style.backgroundColor = "#ff4444";
+ this.errorContainer.style.color = "#fff";
+ this.errorContainer.style.borderRadius = "4px";
+ this.container.style.position = "relative";
+ this.container.appendChild(this.errorContainer);
+ }
+ this.errorContainer.textContent = message;
+ this.errorContainer.style.display = "block";
+ }
+
+ private hideError() {
+ if (this.errorContainer) {
+ this.errorContainer.style.display = "none";
+ }
+ }
+
+ private formatError(error: unknown): string {
+ const message = error instanceof Error ? error.message : `${error}`;
+ return message.startsWith("Error: ")
+ ? message.slice("Error: ".length)
+ : message;
+ }
+
+ private mapSeverity(severity: Severity): monaco.MarkerSeverity {
+ switch (severity) {
+ case Severity.Info:
+ return monaco.MarkerSeverity.Info;
+ case Severity.Warning:
+ return monaco.MarkerSeverity.Warning;
+ case Severity.Error:
+ return monaco.MarkerSeverity.Error;
+ case Severity.Fatal:
+ return monaco.MarkerSeverity.Error;
+ }
+ }
+
+ dispose() {
+ this.languageServer?.dispose();
+ this.editor?.dispose();
+ if (this.workspace && this.fileHandle) {
+ try {
+ this.workspace.closeFile(this.fileHandle);
+ } catch (err) {
+ console.warn("Error closing file:", err);
+ }
+ }
+ if (this.checkTimeoutId !== null) {
+ window.clearTimeout(this.checkTimeoutId);
+ }
+ }
+}
+
+class LanguageServer
+ implements
+ monaco.languages.DefinitionProvider,
+ monaco.languages.HoverProvider,
+ monaco.languages.CompletionItemProvider,
+ monaco.languages.InlayHintsProvider,
+ monaco.languages.CodeActionProvider
+{
+ private definitionDisposable: monaco.IDisposable;
+ private hoverDisposable: monaco.IDisposable;
+ private completionDisposable: monaco.IDisposable;
+ private inlayHintsDisposable: monaco.IDisposable;
+ private codeActionDisposable: monaco.IDisposable;
+ private diagnostics: Diagnostic[] = [];
+
+ constructor(
+ private workspace: Workspace,
+ private fileHandle: FileHandle,
+ ) {
+ this.definitionDisposable = monaco.languages.registerDefinitionProvider(
+ "python",
+ this,
+ );
+ this.hoverDisposable = monaco.languages.registerHoverProvider(
+ "python",
+ this,
+ );
+ this.completionDisposable =
+ monaco.languages.registerCompletionItemProvider("python", this);
+ this.inlayHintsDisposable = monaco.languages.registerInlayHintsProvider(
+ "python",
+ this,
+ );
+ this.codeActionDisposable = monaco.languages.registerCodeActionProvider(
+ "python",
+ this,
+ );
+ }
+
+ updateDiagnostics(diagnostics: Diagnostic[]) {
+ this.diagnostics = diagnostics;
+ }
+
+ triggerCharacters = ["."];
+
+ provideCompletionItems(
+ _model: monaco.editor.ITextModel,
+ position: monaco.Position,
+ ): monaco.languages.ProviderResult {
+ try {
+ const completions = this.workspace.completions(
+ this.fileHandle,
+ new TyPosition(position.lineNumber, position.column),
+ );
+
+ const digitsLength = String(completions.length - 1).length;
+
+ return {
+ suggestions: completions.map((completion, i) => ({
+ label: {
+ label: completion.name,
+ detail:
+ completion.module_name == null
+ ? undefined
+ : ` (import ${completion.module_name})`,
+ description: completion.detail ?? undefined,
+ },
+ sortText: String(i).padStart(digitsLength, "0"),
+ kind:
+ completion.kind == null
+ ? monaco.languages.CompletionItemKind.Variable
+ : mapCompletionKind(completion.kind),
+ insertText: completion.insert_text ?? completion.name,
+ additionalTextEdits: completion.additional_text_edits?.map(
+ (edit: TextEdit) => ({
+ range: tyRangeToMonacoRange(edit.range),
+ text: edit.new_text,
+ }),
+ ),
+ documentation: completion.documentation,
+ detail: completion.detail,
+ range: undefined as any,
+ })),
+ };
+ } catch (err) {
+ console.warn("Error providing completions:", err);
+ return undefined;
+ }
+ }
+
+ provideHover(
+ _model: monaco.editor.ITextModel,
+ position: monaco.Position,
+ ): monaco.languages.ProviderResult {
+ try {
+ const hover = this.workspace.hover(
+ this.fileHandle,
+ new TyPosition(position.lineNumber, position.column),
+ );
+
+ if (hover == null) {
+ return undefined;
+ }
+
+ return {
+ range: tyRangeToMonacoRange(hover.range),
+ contents: [{ value: hover.markdown, isTrusted: true }],
+ };
+ } catch (err) {
+ console.warn("Error providing hover:", err);
+ return undefined;
+ }
+ }
+
+ provideDefinition(
+ model: monaco.editor.ITextModel,
+ position: monaco.Position,
+ ): monaco.languages.ProviderResult<
+ monaco.languages.Definition | monaco.languages.LocationLink[]
+ > {
+ try {
+ const links = this.workspace.gotoDefinition(
+ this.fileHandle,
+ new TyPosition(position.lineNumber, position.column),
+ );
+
+ if (links.length === 0) {
+ return undefined;
+ }
+
+ const currentUri = model.uri;
+ const results = links
+ .filter((link: LocationLink) => {
+ const linkUri = monaco.Uri.parse(link.path);
+ return linkUri.path === currentUri.path;
+ })
+ .map((link: LocationLink) => ({
+ uri: currentUri,
+ range: tyRangeToMonacoRange(link.full_range),
+ targetSelectionRange:
+ link.selection_range == null
+ ? undefined
+ : tyRangeToMonacoRange(link.selection_range),
+ originSelectionRange:
+ link.origin_selection_range == null
+ ? undefined
+ : tyRangeToMonacoRange(link.origin_selection_range),
+ }));
+
+ return results.length > 0 ? results : undefined;
+ } catch (err) {
+ console.warn("Error providing definition:", err);
+ return undefined;
+ }
+ }
+
+ provideInlayHints(
+ _model: monaco.editor.ITextModel,
+ range: monaco.IRange,
+ ): monaco.languages.ProviderResult {
+ try {
+ const inlayHints = this.workspace.inlayHints(
+ this.fileHandle,
+ monacoRangeToTyRange(range),
+ );
+
+ if (inlayHints.length === 0) {
+ return undefined;
+ }
+
+ return {
+ dispose: () => {},
+ hints: inlayHints.map((hint) => ({
+ label: hint.label.map((part) => ({
+ label: part.label,
+ })),
+ position: {
+ lineNumber: hint.position.line,
+ column: hint.position.column,
+ },
+ kind: mapInlayHintKind(hint.kind),
+ textEdits: hint.text_edits.map((edit: TextEdit) => ({
+ range: tyRangeToMonacoRange(edit.range),
+ text: edit.new_text,
+ })),
+ })),
+ };
+ } catch (err) {
+ console.warn("Error providing inlay hints:", err);
+ return undefined;
+ }
+ }
+
+ provideCodeActions(
+ model: monaco.editor.ITextModel,
+ range: monaco.Range,
+ ): monaco.languages.ProviderResult {
+ const actions: monaco.languages.CodeAction[] = [];
+
+ for (const diagnostic of this.diagnostics) {
+ const diagnosticRange = diagnostic.range;
+ if (diagnosticRange == null) {
+ continue;
+ }
+
+ const monacoRange = tyRangeToMonacoRange(diagnosticRange);
+ if (!monaco.Range.areIntersecting(range, new monaco.Range(
+ monacoRange.startLineNumber,
+ monacoRange.startColumn,
+ monacoRange.endLineNumber,
+ monacoRange.endColumn,
+ ))) {
+ continue;
+ }
+
+ try {
+ const codeActions = this.workspace.codeActions(
+ this.fileHandle,
+ diagnostic.raw,
+ );
+ if (codeActions == null) {
+ continue;
+ }
+
+ for (const codeAction of codeActions) {
+ actions.push({
+ title: codeAction.title,
+ kind: "quickfix",
+ isPreferred: codeAction.preferred,
+ edit: {
+ edits: codeAction.edits.map((edit) => ({
+ resource: model.uri,
+ textEdit: {
+ range: tyRangeToMonacoRange(edit.range),
+ text: edit.new_text,
+ },
+ versionId: model.getVersionId(),
+ })),
+ },
+ });
+ }
+ } catch (err) {
+ console.warn("Error getting code actions:", err);
+ }
+ }
+
+ if (actions.length === 0) {
+ return undefined;
+ }
+
+ return {
+ actions,
+ dispose: () => {},
+ };
+ }
+
+ dispose() {
+ this.definitionDisposable.dispose();
+ this.hoverDisposable.dispose();
+ this.completionDisposable.dispose();
+ this.inlayHintsDisposable.dispose();
+ this.codeActionDisposable.dispose();
+ }
+}
+
+// Helper functions
+function tyRangeToMonacoRange(range: TyRange): monaco.IRange {
+ return {
+ startLineNumber: range.start.line,
+ startColumn: range.start.column,
+ endLineNumber: range.end.line,
+ endColumn: range.end.column,
+ };
+}
+
+function monacoRangeToTyRange(range: monaco.IRange): TyRange {
+ return new TyRange(
+ new TyPosition(range.startLineNumber, range.startColumn),
+ new TyPosition(range.endLineNumber, range.endColumn),
+ );
+}
+
+function mapInlayHintKind(kind: InlayHintKind): monaco.languages.InlayHintKind {
+ switch (kind) {
+ case InlayHintKind.Type:
+ return monaco.languages.InlayHintKind.Type;
+ case InlayHintKind.Parameter:
+ return monaco.languages.InlayHintKind.Parameter;
+ }
+}
+
+function mapCompletionKind(
+ kind: CompletionKind,
+): monaco.languages.CompletionItemKind {
+ switch (kind) {
+ case CompletionKind.Text:
+ return monaco.languages.CompletionItemKind.Text;
+ case CompletionKind.Method:
+ return monaco.languages.CompletionItemKind.Method;
+ case CompletionKind.Function:
+ return monaco.languages.CompletionItemKind.Function;
+ case CompletionKind.Constructor:
+ return monaco.languages.CompletionItemKind.Constructor;
+ case CompletionKind.Field:
+ return monaco.languages.CompletionItemKind.Field;
+ case CompletionKind.Variable:
+ return monaco.languages.CompletionItemKind.Variable;
+ case CompletionKind.Class:
+ return monaco.languages.CompletionItemKind.Class;
+ case CompletionKind.Interface:
+ return monaco.languages.CompletionItemKind.Interface;
+ case CompletionKind.Module:
+ return monaco.languages.CompletionItemKind.Module;
+ case CompletionKind.Property:
+ return monaco.languages.CompletionItemKind.Property;
+ case CompletionKind.Unit:
+ return monaco.languages.CompletionItemKind.Unit;
+ case CompletionKind.Value:
+ return monaco.languages.CompletionItemKind.Value;
+ case CompletionKind.Enum:
+ return monaco.languages.CompletionItemKind.Enum;
+ case CompletionKind.Keyword:
+ return monaco.languages.CompletionItemKind.Keyword;
+ case CompletionKind.Snippet:
+ return monaco.languages.CompletionItemKind.Snippet;
+ case CompletionKind.Color:
+ return monaco.languages.CompletionItemKind.Color;
+ case CompletionKind.File:
+ return monaco.languages.CompletionItemKind.File;
+ case CompletionKind.Reference:
+ return monaco.languages.CompletionItemKind.Reference;
+ case CompletionKind.Folder:
+ return monaco.languages.CompletionItemKind.Folder;
+ case CompletionKind.EnumMember:
+ return monaco.languages.CompletionItemKind.EnumMember;
+ case CompletionKind.Constant:
+ return monaco.languages.CompletionItemKind.Constant;
+ case CompletionKind.Struct:
+ return monaco.languages.CompletionItemKind.Struct;
+ case CompletionKind.Event:
+ return monaco.languages.CompletionItemKind.Event;
+ case CompletionKind.Operator:
+ return monaco.languages.CompletionItemKind.Operator;
+ case CompletionKind.TypeParameter:
+ return monaco.languages.CompletionItemKind.TypeParameter;
+ }
+}
+
diff --git a/playground/ty-embed/src/index.css b/playground/ty-embed/src/index.css
new file mode 100644
index 0000000000..8a7750d689
--- /dev/null
+++ b/playground/ty-embed/src/index.css
@@ -0,0 +1,44 @@
+/* Basic reset for the editor container */
+.ty-embed-container {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ "Helvetica Neue", Arial, sans-serif;
+ box-sizing: border-box;
+}
+
+.ty-embed-container *,
+.ty-embed-container *::before,
+.ty-embed-container *::after {
+ box-sizing: inherit;
+}
+
+/* Loading state */
+.ty-embed-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40px;
+ color: #666;
+}
+
+/* Error message */
+.ty-embed-error {
+ padding: 12px;
+ background-color: #ff4444;
+ color: #fff;
+ border-radius: 4px;
+ margin: 10px;
+}
+
+/* Diagnostics panel */
+.ty-embed-diagnostics {
+ font-family: "Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro",
+ monospace;
+}
+
+.ty-embed-diagnostic-item {
+ transition: background-color 0.15s ease;
+}
+
+.ty-embed-diagnostic-item:hover {
+ filter: brightness(0.95);
+}
diff --git a/playground/ty-embed/src/index.ts b/playground/ty-embed/src/index.ts
new file mode 100644
index 0000000000..980e2c3bd5
--- /dev/null
+++ b/playground/ty-embed/src/index.ts
@@ -0,0 +1,85 @@
+import { EmbeddableEditor, EditorOptions } from "./EmbeddableEditor";
+
+export interface TyEditorOptions extends EditorOptions {
+ container: HTMLElement | string;
+}
+
+/**
+ * Initialize a ty editor instance in the specified container.
+ *
+ * @param options Configuration options for the editor
+ * @returns A handle to control the editor instance
+ *
+ * @example
+ * ```javascript
+ * import { createTyEditor } from 'ty-embed';
+ *
+ * const editor = createTyEditor({
+ * container: '#editor',
+ * initialCode: 'print("Hello, ty!")',
+ * theme: 'dark',
+ * height: '500px'
+ * });
+ * ```
+ */
+export function createTyEditor(options: TyEditorOptions) {
+ const editor = new EmbeddableEditor(options.container, options);
+
+ return {
+ /**
+ * Unmount and cleanup the editor instance
+ */
+ dispose() {
+ editor.dispose();
+ },
+ };
+}
+
+/**
+ * Initialize multiple ty editor instances at once.
+ *
+ * @param selector CSS selector for containers (e.g., '.ty-editor')
+ * @param defaultOptions Default options to apply to all editors
+ * @returns Array of editor handles
+ *
+ * @example
+ * ```html
+ *
+ *
+ *
+ *
+ * ```
+ */
+export function createTyEditors(
+ selector: string,
+ defaultOptions: Partial = {},
+) {
+ const containers = document.querySelectorAll(selector);
+ const editors = [];
+
+ for (const container of Array.from(containers)) {
+ const dataCode = container.getAttribute("data-code");
+ const dataTheme = container.getAttribute("data-theme");
+ const dataHeight = container.getAttribute("data-height");
+ const dataFile = container.getAttribute("data-file");
+
+ const options: TyEditorOptions = {
+ container: container as HTMLElement,
+ ...defaultOptions,
+ initialCode: dataCode ?? defaultOptions.initialCode,
+ theme:
+ (dataTheme as "light" | "dark") ?? defaultOptions.theme ?? "light",
+ height: dataHeight ?? defaultOptions.height ?? "400px",
+ fileName: dataFile ?? defaultOptions.fileName,
+ };
+
+ editors.push(createTyEditor(options));
+ }
+
+ return editors;
+}
+
+// Re-export types
+export type { EditorOptions };
diff --git a/playground/ty-embed/tsconfig.json b/playground/ty-embed/tsconfig.json
new file mode 100644
index 0000000000..3f993887c2
--- /dev/null
+++ b/playground/ty-embed/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "declaration": true,
+ "declarationDir": "./dist/types"
+ },
+ "include": ["src"]
+}
diff --git a/playground/ty-embed/vite.config.ts b/playground/ty-embed/vite.config.ts
new file mode 100644
index 0000000000..add68277ec
--- /dev/null
+++ b/playground/ty-embed/vite.config.ts
@@ -0,0 +1,43 @@
+import { defineConfig } from "vite";
+import { viteStaticCopy } from "vite-plugin-static-copy";
+
+export default defineConfig({
+ plugins: [
+ viteStaticCopy({
+ targets: [
+ {
+ src: ["ty_wasm/*", "!ty_wasm/.gitignore"],
+ dest: "ty_wasm",
+ },
+ ],
+ }),
+ ],
+ build: {
+ lib: {
+ entry: "./src/index.ts",
+ name: "TyEmbed",
+ formats: ["es", "umd"],
+ fileName: (format) => `ty-embed.${format}.js`,
+ },
+ rollupOptions: {
+ external: ["ty_wasm"],
+ output: {
+ assetFileNames: (assetInfo) => {
+ if (assetInfo.name === "style.css") return "ty-embed.css";
+ return assetInfo.name ?? "asset";
+ },
+ paths: {
+ ty_wasm: "./ty_wasm/ty_wasm.js",
+ },
+ },
+ },
+ copyPublicDir: false,
+ },
+ optimizeDeps: {
+ exclude: ["ty_wasm"],
+ },
+ server: {
+ port: 3001,
+ open: "/example-dev.html",
+ },
+});