diff --git a/README.md b/README.md index 1ad7c69..2844b85 100644 --- a/README.md +++ b/README.md @@ -144,10 +144,18 @@ webview to the sidebar. MCP speaks all three of its pillars rather than tools al MCP resources and prompts are offered to the agent, not just listed in a panel. -Where it is still thin, specifically: a `WorkspaceEdit` cannot create, delete or rename -files, and the shim has no custom editors and no debug adapter API. Those three fail loudly — -`WorkspaceEdit` throws, and the other two are absent from the API object, so calling one is a -`TypeError` rather than a quiet nothing. +`WorkspaceEdit` creates, deletes and renames files, all-or-nothing: if one operation cannot be +done, none are, and a file with unsaved edits is never deleted or overwritten. An extension can +own a file type through `registerCustomEditorProvider` — the document stays a normal +`TextDocument`, so edits go through the model and `Ctrl+Z` still works. Extensions can observe +debugging: the active session, its start and end, the breakpoint list, and adding or removing +breakpoints. + +Where it is still thin, specifically: a custom editor that owns the bytes itself +(`CustomEditorProvider`, with its own save and backup) rather than a text document, and an +extension supplying its own debugger (`registerDebugAdapterDescriptorFactory`). Both fail +loudly — the first throws with a message naming what is missing, the second is absent from the +API object, so calling it is a `TypeError` rather than a quiet nothing. Decorations used to sit in that list as the one gap that answered successfully and drew nothing. They now draw: `createTextEditorDecorationType` compiles the requested styling into diff --git a/ide/src/App.tsx b/ide/src/App.tsx index ba88aed..1f2f9dd 100644 --- a/ide/src/App.tsx +++ b/ide/src/App.tsx @@ -70,6 +70,8 @@ import { ImagePane, MarkdownPane, isImage, mdToHtml } from "./editor/MediaPane"; import monaco, { languageOf, applyTsPaths, revalidateTs } from "./editor/monacoSetup"; import * as projectModels from "./editor/projectModels"; import * as proposalDeco from "./editor/proposalDeco"; +import { CustomEditorPane } from "./editor/CustomEditorPane"; +import { editorFor as customEditorFor } from "./ext/customEditors"; import * as symbolIndex from "./editor/symbolIndex"; import * as vscodeShim from "./ext/vscodeShim"; import { missingFor as missingLspFor, shouldTell as shouldTellLsp, type ServerRow } from "./engine/lspHint"; @@ -8218,7 +8220,12 @@ ${(r.output || "").slice(0, 2000)}`; const realFile = !!(s.workspace && !diffMeta); const isImg = realFile && isImage(activeRel); const isMdPrev = realFile && activeRel.endsWith(".md") && !!s.mdPreview[activeRel]; - const isReal = realFile && !isImg && !isMdPrev; + // 확장이 만든 편집기가 이 파일을 맡는가. 선언과 구현이 둘 다 있어야 한다 — + // 선언만 있으면 텍스트로 연다(빈 화면보다 낫다). + const custom = realFile + ? customEditorFor(activeRel, extHost.getCustomEditorDecls(), new Set(extHost.listExtEditors().map(e => e.viewType))) + : null; + const isReal = realFile && !isImg && !isMdPrev && !custom; return (
{ this._focusSlot = si; }} @@ -8240,6 +8247,12 @@ ${(r.output || "").slice(0, 2000)}`; ) : isMdPrev ? ( + ) : custom ? ( + ) : isReal ? ( (null); + const [err, setErr] = useState(""); + const frameRef = useRef(null); + + useEffect(() => { + let dead = false; + const ed = extHost.extEditorFor(viewType); + if (!ed) { setErr(t("exth.customEditorGone", { viewType })); return; } + // 문서를 먼저 세운다 — 이 파일에는 모델이 없을 수 있고, 그러면 확장이 + // document.getText() 첫 줄에서 던진다. + extHost.openDocFor(rel) + .then(doc => { + if (dead) return null; + if (!doc) throw new Error(t("exth.customEditorNoDoc", { rel })); + return ed.resolve(rel, doc); + }) + .then(h => { if (!dead && h != null) setHtml(String(h)); }) + // 확장이 던지면 빈 화면 대신 이유를 띄운다 — 안 그러면 파일이 안 열린 것처럼 보인다. + .catch(e => { if (!dead) setErr(e instanceof Error ? e.message : String(e)); }); + return () => { dead = true; }; + }, [viewType, rel]); + + // 웹뷰가 보낸 말을 확장에게 넘긴다. 사이드바 웹뷰와 같은 봉투를 쓴다. + useEffect(() => { + const onMsg = (e: MessageEvent) => { + const d: any = e.data; + if (!d || d.__schutzView !== "editor:" + rel) return; + extHost.extEditorFor(viewType)?.post(d.data); + }; + window.addEventListener("message", onMsg); + return () => window.removeEventListener("message", onMsg); + }, [viewType, rel]); + + if (err) { + return
⚠️ {err}
; + } + if (html == null) { + return
{t("exth.customEditorLoading")}
; + } + return ( +