From 0d7e21d093b67f8710e69d210ac79e2284ac1175 Mon Sep 17 00:00:00 2001 From: Seungpyo1007 Date: Tue, 11 Aug 2026 13:49:47 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=95=EC=9E=A5=EC=9D=B4=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=ED=98=95=EC=8B=9D=EC=9D=84=20=EB=A7=A1=EC=9D=84=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EA=B2=8C=20(CustomTextEditorProvider)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit contributes.customEditors 선언 + registerCustomEditorProvider 구현이 둘 다 있어야 그 파일이 그 편집기로 열린다. 선언만 있으면 텍스트로 연다 — 확장이 아직 안 떴을 때 빈 화면 띄우는 것보다 낫다. 문서는 평범한 TextDocument 라 확장이 고칠 때 WorkspaceEdit 을 쓴다. 모델을 거치니까 Ctrl+Z 도 되고 저장 기준선도 안 어긋난다. 바이너리를 직접 들고 저장·백업까지 하는 CustomEditorProvider 는 아직 없음. resolveCustomTextEditor 가 없으면 등록 시점에 던진다. 조용히 등록해두면 그 파일이 빈 화면으로 열린다. 만들면서 나온 것 둘. - .draw 같은 파일은 모델이 없다. preload 는 TS 계열만 세우고 커스텀 편집기가 맡으면 Monaco 페인도 안 뜨니까. 확장에 null 이 가서 document.getText() 가 첫 줄에서 던졌다. 없으면 읽어서 세우게 함. - iframe 에 acquireVsCodeApi 를 안 넣어서 웹뷰 스크립트가 죽었다. 사이드바 웹뷰가 쓰는 주입기를 그대로 쓴다. README 도 실제 상태로. WorkspaceEdit 파일 조작과 디버그 관찰은 이미 되는데 "없다" 고 적혀 있었다. --- README.md | 16 +++-- ide/src/App.tsx | 15 +++- ide/src/editor/CustomEditorPane.tsx | 64 +++++++++++++++++ ide/src/ext/customEditors.test.ts | 107 ++++++++++++++++++++++++++++ ide/src/ext/customEditors.ts | 91 +++++++++++++++++++++++ ide/src/ext/extHost.ts | 32 ++++++++- ide/src/ext/vscodeShim.ts | 53 ++++++++++++++ ide/src/i18n/dict/exth.ts | 13 ++++ ide/vitest.config.ts | 2 +- 9 files changed, 386 insertions(+), 7 deletions(-) create mode 100644 ide/src/editor/CustomEditorPane.tsx create mode 100644 ide/src/ext/customEditors.test.ts create mode 100644 ide/src/ext/customEditors.ts 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 ( +