Skip to content

Commit 7433881

Browse files
feat(vscode-bindings): expand surface for rsr-certifier port (#64) (#102)
* 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> * fixup: add stringIsEmpty helper (used by rsr-certifier no-workspace checks) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e2d5572 commit 7433881

3 files changed

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

155323
const VscodeLanguageClient = {

0 commit comments

Comments
 (0)