/* ── MonacoEditor — Full code editor with InlineCompletion ghost text and Tab-to-accept ── */ import { useRef, useCallback, useEffect } from "react"; import Editor, { type OnMount } from "@monaco-editor/react"; import type { editor } from "monaco-editor"; export interface InlineSuggestion { insertText: string; range?: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number; }; } interface MonacoEditorProps { filename: string; content: string; language?: string; suggestions?: InlineSuggestion[]; onContentChange?: (value: string) => void; readOnly?: boolean; } /* Map file extensions to Monaco language IDs */ function extToLanguage(filename: string): string { const ext = filename.split(".").pop()?.toLowerCase() || "txt"; const map: Record = { ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", py: "python", rs: "rust", go: "go", rb: "ruby", java: "java", kt: "kotlin", swift: "swift", cpp: "cpp", c: "c", h: "c", cs: "csharp", php: "php", html: "html", css: "css", scss: "scss", less: "less", json: "json", yaml: "yaml", yml: "yaml", md: "markdown", sql: "sql", sh: "shell", bash: "shell", dockerfile: "dockerfile", toml: "plaintext", xml: "xml", vue: "html", svelte: "html", }; return map[ext] || "plaintext"; } export function MonacoEditor({ filename, content, language, suggestions = [], onContentChange, readOnly = false, }: MonacoEditorProps) { const editorRef = useRef(null); const monacoRef = useRef(null); const disposerRef = useRef<{ dispose: () => void } | null>(null); const currentSuggestions = useRef(suggestions); currentSuggestions.current = suggestions; const lang = language || extToLanguage(filename); const handleEditorMount: OnMount = useCallback((ed, monaco) => { editorRef.current = ed; monacoRef.current = monaco; /* Register inline completions provider for all languages */ const languages = monaco.languages as unknown as { registerInlineCompletionsProvider: ( lang: string, provider: Record, ) => { dispose: () => void }; }; const disposer = languages.registerInlineCompletionsProvider("*", { provideInlineCompletions: ( _model: unknown, position: { lineNumber: number; column: number }, ) => { const items = currentSuggestions.current.map( (s: InlineSuggestion) => ({ insertText: s.insertText, range: s.range ? new monaco.Range( s.range.startLineNumber, s.range.startColumn, s.range.endLineNumber, s.range.endColumn, ) : new monaco.Range( position.lineNumber, position.column, position.lineNumber, position.column, ), }), ); return { items }; }, handleRejection: () => { /* suggestion rejected */ }, }); disposerRef.current = disposer; /* Unbind default Tab indent to prevent conflict with inline suggestion accept. Monaco already accepts inline suggestions on Tab when inlineSuggest.enabled is true, so no custom action is needed. */ }, []); /* Clean up disposer on unmount */ useEffect(() => { return () => { disposerRef.current?.dispose(); }; }, []); /* Update content when external content changes */ useEffect(() => { const editor = editorRef.current; if (!editor || !monacoRef.current) return; const current = editor.getValue(); if (current !== content) { editor.setValue(content); } }, [content]); return (
{/* Tab bar */}
{filename || "untitled"} {lang} {/* Inline Completions indicator */} {suggestions.length > 0 && ( Tab ⤶ )}
{/* Monaco Editor */}
{ if (value !== undefined) onContentChange?.(value); }} onMount={handleEditorMount} options={{ readOnly, fontSize: 13, fontFamily: "'Geist Mono', 'JetBrains Mono', 'Fira Code', monospace", lineNumbers: "on", minimap: { enabled: false }, scrollBeyondLastLine: false, wordWrap: "on", tabSize: 2, renderWhitespace: "selection", padding: { top: 8 }, suggestOnTriggerCharacters: true, quickSuggestions: true, inlineSuggest: { enabled: true }, "semanticHighlighting.enabled": true, smoothScrolling: true, cursorBlinking: "smooth", cursorSmoothCaretAnimation: "on", formatOnPaste: true, autoClosingBrackets: "always", autoClosingQuotes: "always", autoIndent: "full", guides: { bracketPairs: true, indentation: true, }, unicodeHighlight: { ambiguousCharacters: false }, }} />
{/* Status bar */}
{filename} · {lang.toUpperCase()} {suggestions.length > 0 && ( Tab accept suggestion )} Monaco
); }