Skip to content

Commit 8c62b2e

Browse files
hyperpolymathclaude
andcommitted
feat(vscode-bindings): expand surface for rsr-certifier port (#64)
Adds ~20 new extern fns + 5 opaque handle types to stdlib/Vscode.affine and their JS-side implementations to packages/affine-vscode/mod.js, so the rsr-certifier extension in hyperpolymath/standards can be ported to .affine with full feature parity (issue #64 acceptance bullet 2). Bindings added — see stdlib/Vscode.affine for shapes: Workspace: workspaceFolderFirstPath, workspaceRootUri Uri / file-system / text documents: uriFromPath, uriJoinPath, uriPath, fsWriteFile, openTextDocument, showTextDocument Status bar: createStatusBarItem + setText / setTooltip / setCommand / setBackgroundColorTheme / show / hide / asDisposable Diagnostics (JSON-shaped to avoid Range/Diagnostic struct FFI): createDiagnosticCollection + clear / setForUri / asDisposable Webview: createWebviewPanel + setHtml / asDisposable Clipboard: clipboardWriteText Events: onDidSaveTextDocument (handler is a zero-arg wasm-table thunk; the TextDocument arg from the underlying vscode event is dropped at the FFI boundary — handlers that need the saved file path can call editorActiveFilePath() inside the thunk) Path helpers / process / context: pathBasename, pathJoin, processPlatform, extensionAbsolutePath New opaque types: StatusBarItem, DiagnosticCollection, WebviewPanel, Uri, TextDocument. Two API shapes deliberately omitted because they require an async-extern ABI that doesn't exist yet: vscode.window.withProgress(opts, async task) -- task is Thenable-returning. LanguageClient.sendRequest(method, params) -- returns a Thenable. Extensions that need either should fall back to terminal / execSync shells until async externs land. Both omissions are noted in stdlib/Vscode.affine and in packages/affine-vscode/README.adoc. Verification: $ AFFINESCRIPT_STDLIB=$PWD/stdlib affinescript compile \ /tmp/test-new-bindings.affine -o /tmp/test-bindings.cjs Compiled ... -> /tmp/test-bindings.cjs (Node-CJS) A smoke-test .affine exercising every new binding compiles cleanly via the existing affinescript compiler — no codegen changes needed; the new symbols flow through the existing extern-resolution + Wasm-import- emission paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e2d5572 commit 8c62b2e

3 files changed

Lines changed: 370 additions & 7 deletions

File tree

packages/affine-vscode/README.adoc

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,43 @@ adapter._extraImports = () => makeBindings(vscode, lc, () => instance);
3636

3737
The adapter implements every `extern fn` declared in:
3838

39-
* `stdlib/Vscode.affine` — 11 bindings covering vscode.commands.registerCommand,
39+
* `stdlib/Vscode.affine` — initial set (issue #35 Phase 2): commands.registerCommand,
4040
workspace.getConfiguration / createFileSystemWatcher,
41-
window.showErrorMessage / showWarningMessage / showInformationMessage,
41+
window.{showError,showWarning,showInformation}Message,
4242
window.createTerminal + terminal.show / sendText,
43-
ExtensionContext.subscriptions.push, and window.activeTextEditor.
43+
ExtensionContext.subscriptions.push, window.activeTextEditor,
44+
editorActive{FilePath,LanguageId}, workspaceConfigGet{Bool,String},
45+
consoleLog, execSync, and three string helpers.
46+
+
47+
Expanded 2026-05-11 for the rsr-certifier port (issue #64):
48+
workspaceFolderFirstPath / workspaceRootUri, uriFromPath / uriJoinPath / uriPath,
49+
fsWriteFile, openTextDocument / showTextDocument,
50+
createStatusBarItem + statusBarItem.{setText,setTooltip,setCommand,setBackgroundColorTheme,show,hide,asDisposable},
51+
createDiagnosticCollection + diagnosticCollection.{clear,setForUri,asDisposable},
52+
createWebviewPanel + webviewPanel.{setHtml,asDisposable},
53+
clipboardWriteText, onDidSaveTextDocument,
54+
pathBasename / pathJoin / processPlatform, and extensionAbsolutePath.
55+
4456
* `stdlib/VscodeLanguageClient.affine` — 3 bindings covering
4557
new LanguageClient(...) / start() / stop().
4658

59+
== Deliberate omissions
60+
61+
Two API shapes are not bound because they cannot be expressed in the
62+
current synchronous extern-call ABI without an async-extern hookup:
63+
64+
* `vscode.window.withProgress(opts, async task)` — the second arg is an
65+
async Thenable-returning task.
66+
* `LanguageClient.sendRequest(method, params)` — returns a Thenable.
67+
68+
Extensions that need either should fall back to shelling out to a CLI
69+
via `Vscode::createTerminal` / `Vscode::execSync` until an async-extern
70+
ABI lands.
71+
4772
== Design notes
4873

49-
* All host objects (Disposable, Terminal, ExtensionContext, ...) are
74+
* All host objects (Disposable, Terminal, ExtensionContext, StatusBarItem,
75+
DiagnosticCollection, WebviewPanel, Uri, TextDocument, ...) are
5076
represented as opaque integer handles on both sides of the FFI. The
5177
adapter maintains a JS-side handle table.
5278
* String args are passed across as i32 pointers into the wasm linear
@@ -55,9 +81,14 @@ The adapter implements every `extern fn` declared in:
5581
* Wasm function-pointer args (e.g. command handlers) come in as
5682
`__indirect_function_table` indices; the adapter wraps each in a JS
5783
thunk that re-enters the wasm module on invocation.
84+
* Diagnostics avoid a Range+Diagnostic struct FFI by accepting a JSON
85+
array: `[{startLine,startCol,endLine,endCol,message,severity}, ...]`.
86+
The adapter parses it and constructs the vscode objects.
5887

5988
== Status
6089

61-
Phase 2 — bindings landed. Phase 3 (`editors/vscode/src/extension.ts` →
62-
`editors/vscode/src/extension.affine` rewrite) is the next milestone. Phase 4
63-
sweeps the rattlescript-face copy.
90+
Phase 2 — bindings landed. The pilot affinescript port (issue #63) and
91+
external-extension ports (issue #64: my-lang, rsr-certifier) consume
92+
this adapter; the long-term plan is to publish it to npm so consumers
93+
do not have to vendor `mod.js`. Phase 4 (rattlescript-face sweep) still
94+
to do.

packages/affine-vscode/mod.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,173 @@ module.exports = function makeVscodeBindings(vscode, lcModule, hostShim) {
150150
const out = s.endsWith(suffix) ? s.slice(0, -suffix.length) + replacement : s;
151151
return reg(out);
152152
},
153+
154+
// ── Workspace ───────────────────────────────────────────────────
155+
workspaceFolderFirstPath: () => {
156+
const folders = vscode.workspace.workspaceFolders;
157+
const first = folders && folders[0];
158+
return reg(first ? first.uri.fsPath : "");
159+
},
160+
workspaceRootUri: () => {
161+
const folders = vscode.workspace.workspaceFolders;
162+
const first = folders && folders[0];
163+
return first ? reg(first.uri) : 0;
164+
},
165+
166+
// ── URI / file-system / text documents ─────────────────────────
167+
uriFromPath: (pathPtr) => reg(vscode.Uri.file(readString(pathPtr))),
168+
uriJoinPath: (baseHandle, segPtr) => {
169+
const base = get(baseHandle);
170+
if (!base) return 0;
171+
return reg(vscode.Uri.joinPath(base, readString(segPtr)));
172+
},
173+
uriPath: (uHandle) => {
174+
const u = get(uHandle);
175+
return reg(u ? u.fsPath : "");
176+
},
177+
fsWriteFile: (uHandle, contentPtr) => {
178+
const u = get(uHandle);
179+
if (!u) return 1;
180+
try {
181+
// Fire-and-forget the Thenable. The host serialises FS ops so
182+
// a subsequent openTextDocument on the same URI sees the file.
183+
vscode.workspace.fs.writeFile(u, Buffer.from(readString(contentPtr)));
184+
return 0;
185+
} catch (e) {
186+
return 1;
187+
}
188+
},
189+
openTextDocument: (uHandle) => {
190+
const u = get(uHandle);
191+
if (!u) return 0;
192+
// openTextDocument returns a Thenable<TextDocument>. The synchronous
193+
// FFI returns a handle to the Thenable itself; showTextDocument is
194+
// also Thenable-returning and chains via vscode's internal queue,
195+
// so this works in practice for the open-then-show pattern.
196+
return reg(vscode.workspace.openTextDocument(u));
197+
},
198+
showTextDocument: (dHandle) => {
199+
const d = get(dHandle);
200+
if (!d) return 1;
201+
// If `d` is itself a Thenable<TextDocument>, vscode unwraps it.
202+
Promise.resolve(d).then((doc) => vscode.window.showTextDocument(doc));
203+
return 0;
204+
},
205+
206+
// ── Status bar ─────────────────────────────────────────────────
207+
createStatusBarItem: (alignment, priority) => {
208+
const align = alignment === 1
209+
? vscode.StatusBarAlignment.Right
210+
: vscode.StatusBarAlignment.Left;
211+
return reg(vscode.window.createStatusBarItem(align, priority));
212+
},
213+
statusBarItemSetText: (sHandle, tPtr) => {
214+
const s = get(sHandle);
215+
if (s) s.text = readString(tPtr);
216+
return 0;
217+
},
218+
statusBarItemSetTooltip: (sHandle, tPtr) => {
219+
const s = get(sHandle);
220+
if (s) s.tooltip = readString(tPtr);
221+
return 0;
222+
},
223+
statusBarItemSetCommand: (sHandle, cPtr) => {
224+
const s = get(sHandle);
225+
if (s) s.command = readString(cPtr);
226+
return 0;
227+
},
228+
statusBarItemSetBackgroundColorTheme: (sHandle, cPtr) => {
229+
const s = get(sHandle);
230+
if (!s) return 0;
231+
const name = readString(cPtr);
232+
s.backgroundColor = name.length === 0 ? undefined : new vscode.ThemeColor(name);
233+
return 0;
234+
},
235+
statusBarItemShow: (sHandle) => { const s = get(sHandle); if (s) s.show(); return 0; },
236+
statusBarItemHide: (sHandle) => { const s = get(sHandle); if (s) s.hide(); return 0; },
237+
statusBarItemAsDisposable: (sHandle) => sHandle, // same JS object is a Disposable
238+
239+
// ── Diagnostics ────────────────────────────────────────────────
240+
createDiagnosticCollection: (namePtr) =>
241+
reg(vscode.languages.createDiagnosticCollection(readString(namePtr))),
242+
diagnosticCollectionClear: (cHandle) => {
243+
const c = get(cHandle);
244+
if (c) c.clear();
245+
return 0;
246+
},
247+
diagnosticCollectionSetForUri: (cHandle, uHandle, jsonPtr) => {
248+
const c = get(cHandle);
249+
const u = get(uHandle);
250+
if (!c || !u) return 1;
251+
let arr;
252+
try { arr = JSON.parse(readString(jsonPtr)); }
253+
catch (e) { return 2; }
254+
if (!Array.isArray(arr)) return 3;
255+
const diagnostics = arr.map((d) => {
256+
const range = new vscode.Range(
257+
d.startLine | 0, d.startCol | 0,
258+
d.endLine | 0, d.endCol | 0
259+
);
260+
const severity = [
261+
vscode.DiagnosticSeverity.Error,
262+
vscode.DiagnosticSeverity.Warning,
263+
vscode.DiagnosticSeverity.Information,
264+
vscode.DiagnosticSeverity.Hint,
265+
][Math.max(0, Math.min(3, d.severity | 0))];
266+
return new vscode.Diagnostic(range, String(d.message ?? ""), severity);
267+
});
268+
c.set(u, diagnostics);
269+
return 0;
270+
},
271+
diagnosticCollectionAsDisposable: (cHandle) => cHandle,
272+
273+
// ── Webview ────────────────────────────────────────────────────
274+
createWebviewPanel: (vtPtr, titlePtr, vc) => {
275+
const viewColumn =
276+
vc === 2 ? vscode.ViewColumn.Two :
277+
vc === 3 ? vscode.ViewColumn.Three :
278+
vscode.ViewColumn.One;
279+
return reg(vscode.window.createWebviewPanel(
280+
readString(vtPtr), readString(titlePtr), viewColumn, {}
281+
));
282+
},
283+
webviewPanelSetHtml: (pHandle, htmlPtr) => {
284+
const p = get(pHandle);
285+
if (p) p.webview.html = readString(htmlPtr);
286+
return 0;
287+
},
288+
webviewPanelAsDisposable: (pHandle) => pHandle,
289+
290+
// ── Clipboard ──────────────────────────────────────────────────
291+
clipboardWriteText: (tPtr) => {
292+
try {
293+
vscode.env.clipboard.writeText(readString(tPtr));
294+
return 0;
295+
} catch (e) {
296+
return 1;
297+
}
298+
},
299+
300+
// ── Events ─────────────────────────────────────────────────────
301+
onDidSaveTextDocument: (handlerIdx) => {
302+
const thunk = wrapHandler(handlerIdx);
303+
// The vscode event ships a TextDocument; we deliberately drop it at
304+
// the FFI boundary (see Vscode.affine docstring). Handlers that
305+
// need the saved file path can call editorActiveFilePath().
306+
return reg(vscode.workspace.onDidSaveTextDocument(() => thunk()));
307+
},
308+
309+
// ── Path helpers ───────────────────────────────────────────────
310+
pathBasename: (pPtr) => reg(require("path").basename(readString(pPtr))),
311+
pathJoin: (aPtr, bPtr) =>
312+
reg(require("path").join(readString(aPtr), readString(bPtr))),
313+
processPlatform: () => reg(process.platform),
314+
315+
// ── ExtensionContext helpers ───────────────────────────────────
316+
extensionAbsolutePath: (ctxHandle, relPtr) => {
317+
const ctx = get(ctxHandle);
318+
return reg(ctx ? ctx.asAbsolutePath(readString(relPtr)) : "");
319+
},
153320
};
154321

155322
const VscodeLanguageClient = {

0 commit comments

Comments
 (0)