main
Zef Hemel 2025-01-08 17:09:09 +01:00
parent 29e127ca78
commit c2fea2d25b
13 changed files with 258 additions and 32 deletions

View File

@ -62,14 +62,16 @@ export class SpaceLuaEnvironment {
for (const globalName of this.env.keys()) { for (const globalName of this.env.keys()) {
const value = this.env.get(globalName); const value = this.env.get(globalName);
if (value instanceof LuaFunction) { if (value instanceof LuaFunction) {
console.log("Now registering Lua function", globalName); console.log(
`[Lua] Registering global function '${globalName}' (source: ${value.body.ctx.ref})`,
);
scriptEnv.registerFunction({ name: globalName }, (...args: any[]) => { scriptEnv.registerFunction({ name: globalName }, (...args: any[]) => {
const sf = new LuaStackFrame(new LuaEnv(), value.body.ctx); const sf = new LuaStackFrame(new LuaEnv(), value.body.ctx);
return luaValueToJS(value.call(sf, ...args.map(jsToLuaValue))); return luaValueToJS(value.call(sf, ...args.map(jsToLuaValue)));
}); });
} }
} }
console.log("Loaded", allScripts.length, "Lua scripts"); console.log("[Lua] Loaded", allScripts.length, "scripts");
} }
} }

View File

@ -3,6 +3,7 @@ import {
LuaEnv, LuaEnv,
LuaNativeJSFunction, LuaNativeJSFunction,
LuaStackFrame, LuaStackFrame,
LuaTable,
luaValueToJS, luaValueToJS,
singleResult, singleResult,
} from "./runtime.ts"; } from "./runtime.ts";
@ -11,9 +12,9 @@ import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts";
import { evalExpression, evalStatement } from "./eval.ts"; import { evalExpression, evalStatement } from "./eval.ts";
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts"; import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
function evalExpr(s: string, e = new LuaEnv()): any { function evalExpr(s: string, e = new LuaEnv(), sf?: LuaStackFrame): any {
const node = parse(`e(${s})`).statements[0] as LuaFunctionCallStatement; const node = parse(`e(${s})`).statements[0] as LuaFunctionCallStatement;
const sf = new LuaStackFrame(e, node.ctx); sf = sf || new LuaStackFrame(e, node.ctx);
return evalExpression( return evalExpression(
node.call.args[0], node.call.args[0],
e, e,
@ -277,3 +278,149 @@ Deno.test("Statement evaluation", async () => {
luaBuildStandardEnv(), luaBuildStandardEnv(),
); );
}); });
Deno.test("Thread local _CTX", async () => {
const env = new LuaEnv();
const threadLocal = new LuaEnv();
threadLocal.setLocal("threadValue", "test123");
const sf = new LuaStackFrame(threadLocal, null);
await evalBlock(
`
function test()
return _CTX.threadValue
end
`,
env,
);
const result = await evalExpr("test()", env, sf);
assertEquals(singleResult(result), "test123");
});
Deno.test("Thread local _CTX - advanced cases", async () => {
// Create environment with standard library
const env = new LuaEnv(luaBuildStandardEnv());
const threadLocal = new LuaEnv();
// Set up some thread local values
threadLocal.setLocal("user", "alice");
threadLocal.setLocal("permissions", new LuaTable());
threadLocal.get("permissions").set("admin", true);
threadLocal.setLocal("data", {
id: 123,
settings: { theme: "dark" },
});
const sf = new LuaStackFrame(threadLocal, null);
// Test 1: Nested function access
await evalBlock(
`
function outer()
local function inner()
return _CTX.user
end
return inner()
end
`,
env,
);
assertEquals(await evalExpr("outer()", env, sf), "alice");
// Test 2: Table access and modification
await evalBlock(
`
function checkAdmin()
return _CTX.permissions.admin
end
function revokeAdmin()
_CTX.permissions.admin = false
return _CTX.permissions.admin
end
`,
env,
);
assertEquals(await evalExpr("checkAdmin()", env, sf), true);
assertEquals(await evalExpr("revokeAdmin()", env, sf), false);
assertEquals(threadLocal.get("permissions").get("admin"), false);
// Test 3: Complex data structures
await evalBlock(
`
function getNestedData()
return _CTX.data.settings.theme
end
function updateTheme(newTheme)
_CTX.data.settings.theme = newTheme
return _CTX.data.settings.theme
end
`,
env,
);
assertEquals(await evalExpr("getNestedData()", env, sf), "dark");
assertEquals(await evalExpr("updateTheme('light')", env, sf), "light");
// Test 4: Multiple thread locals
const threadLocal2 = new LuaEnv();
threadLocal2.setLocal("user", "bob");
const sf2 = new LuaStackFrame(threadLocal2, null);
await evalBlock(
`
function getUser()
return _CTX.user
end
`,
env,
);
// Same function, different thread contexts
assertEquals(await evalExpr("getUser()", env, sf), "alice");
assertEquals(await evalExpr("getUser()", env, sf2), "bob");
// Test 5: Async operations with _CTX
env.set(
"asyncOperation",
new LuaNativeJSFunction(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
return "done";
}),
);
await evalBlock(
`
function asyncTest()
_CTX.status = "starting"
local result = asyncOperation()
_CTX.status = "completed"
return _CTX.status
end
`,
env,
);
assertEquals(await evalExpr("asyncTest()", env, sf), "completed");
assertEquals(threadLocal.get("status"), "completed");
// Test 6: Error handling with _CTX
await evalBlock(
`
function errorTest()
_CTX.error = nil
local status, err = pcall(function()
error("test error")
end)
_CTX.error = "caught"
return _CTX.error
end
`,
env,
);
assertEquals(await evalExpr("errorTest()", env, sf), "caught");
assertEquals(threadLocal.get("error"), "caught");
});

View File

@ -334,11 +334,11 @@ function evalPrefixExpression(
if (prefixValue instanceof Promise) { if (prefixValue instanceof Promise) {
return prefixValue.then(async (resolvedPrefix) => { return prefixValue.then(async (resolvedPrefix) => {
const args = await resolveVarargs(); const args = await resolveVarargs();
return luaCall(resolvedPrefix, args, e.ctx, sf); return luaCall(resolvedPrefix, args, e.ctx, sf.withCtx(e.ctx));
}); });
} else { } else {
return resolveVarargs().then((args) => return resolveVarargs().then((args) =>
luaCall(prefixValue, args, e.ctx, sf) luaCall(prefixValue, args, e.ctx, sf.withCtx(e.ctx))
); );
} }
} }
@ -409,7 +409,7 @@ const operatorsMetaMethods: Record<string, {
"/": { metaMethod: "__div", nativeImplementation: (a, b) => a / b }, "/": { metaMethod: "__div", nativeImplementation: (a, b) => a / b },
"//": { "//": {
metaMethod: "__idiv", metaMethod: "__idiv",
nativeImplementation: (a, b, ctx, sf) => Math.floor(a / b), nativeImplementation: (a, b) => Math.floor(a / b),
}, },
"%": { metaMethod: "__mod", nativeImplementation: (a, b) => a % b }, "%": { metaMethod: "__mod", nativeImplementation: (a, b) => a % b },
"^": { metaMethod: "__pow", nativeImplementation: (a, b) => a ** b }, "^": { metaMethod: "__pow", nativeImplementation: (a, b) => a ** b },

View File

@ -150,6 +150,7 @@ export class LuaFunction implements ILuaFunction {
if (!sf) { if (!sf) {
console.trace(sf); console.trace(sf);
} }
// Set _CTX to the thread local environment from the stack frame
env.setLocal("_CTX", sf.threadLocal); env.setLocal("_CTX", sf.threadLocal);
// Assign the passed arguments to the parameters // Assign the passed arguments to the parameters
@ -271,6 +272,7 @@ export class LuaBuiltinFunction implements ILuaFunction {
} }
call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue { call(sf: LuaStackFrame, ...args: LuaValue[]): Promise<LuaValue> | LuaValue {
// _CTX is already available via the stack frame
return this.fn(sf, ...args); return this.fn(sf, ...args);
} }
@ -608,21 +610,35 @@ export class LuaRuntimeError extends Error {
// Find the line and column // Find the line and column
let line = 1; let line = 1;
let column = 0; let column = 0;
let lastNewline = -1;
for (let i = 0; i < ctx.from; i++) { for (let i = 0; i < ctx.from; i++) {
if (code[i] === "\n") { if (code[i] === "\n") {
line++; line++;
lastNewline = i;
column = 0; column = 0;
} else { } else {
column++; column++;
} }
} }
traceStr += `* ${
ctx.ref || "(unknown source)" // Get the full line of code for context
} @ ${line}:${column}:\n ${code.substring(ctx.from, ctx.to)}\n`; const lineStart = lastNewline + 1;
const lineEnd = code.indexOf("\n", ctx.from);
const codeLine = code.substring(
lineStart,
lineEnd === -1 ? undefined : lineEnd,
);
// Add position indicator
const pointer = " ".repeat(column) + "^";
traceStr += `* ${ctx.ref || "(unknown source)"} @ ${line}:${column}:\n` +
` ${codeLine}\n` +
` ${pointer}\n`;
current = current.parent; current = current.parent;
} }
return `LuaRuntimeError: ${this.message} ${traceStr}`; return `LuaRuntimeError: ${this.message}\nStack trace:\n${traceStr}`;
} }
override toString() { override toString() {

View File

@ -72,8 +72,8 @@ const tonumberFunction = new LuaBuiltinFunction((_sf, value: LuaValue) => {
return Number(value); return Number(value);
}); });
const errorFunction = new LuaBuiltinFunction((_sf, message: string) => { const errorFunction = new LuaBuiltinFunction((sf, message: string) => {
throw new Error(message); throw new LuaRuntimeError(message, sf);
}); });
const pcallFunction = new LuaBuiltinFunction( const pcallFunction = new LuaBuiltinFunction(
@ -81,6 +81,9 @@ const pcallFunction = new LuaBuiltinFunction(
try { try {
return new LuaMultiRes([true, await luaCall(fn, args, sf.astCtx!, sf)]); return new LuaMultiRes([true, await luaCall(fn, args, sf.astCtx!, sf)]);
} catch (e: any) { } catch (e: any) {
if (e instanceof LuaRuntimeError) {
return new LuaMultiRes([false, e.message]);
}
return new LuaMultiRes([false, e.message]); return new LuaMultiRes([false, e.message]);
} }
}, },
@ -91,9 +94,10 @@ const xpcallFunction = new LuaBuiltinFunction(
try { try {
return new LuaMultiRes([true, await fn.call(sf, ...args)]); return new LuaMultiRes([true, await fn.call(sf, ...args)]);
} catch (e: any) { } catch (e: any) {
const errorMsg = e instanceof LuaRuntimeError ? e.message : e.message;
return new LuaMultiRes([ return new LuaMultiRes([
false, false,
await luaCall(errorHandler, [e.message], sf.astCtx!, sf), await luaCall(errorHandler, [errorMsg], sf.astCtx!, sf),
]); ]);
} }
}, },

View File

@ -22,6 +22,7 @@ export const jsApi = new LuaTable({
log: new LuaBuiltinFunction((_sf, ...args) => { log: new LuaBuiltinFunction((_sf, ...args) => {
console.log(...args); console.log(...args);
}), }),
stringify: new LuaBuiltinFunction((_sf, val) => JSON.stringify(val)),
// assignGlobal: new LuaBuiltinFunction((name: string, value: any) => { // assignGlobal: new LuaBuiltinFunction((name: string, value: any) => {
// (globalThis as any)[name] = value; // (globalThis as any)[name] = value;
// }), // }),

View File

@ -56,7 +56,12 @@ function exposeDefinitions(
if (!def.get("name")) { if (!def.get("name")) {
throw new Error("Name is required"); throw new Error("Name is required");
} }
console.log("Registering Lua command", def.get("name")); const fn = def.get(1);
console.log(
`[Lua] Registering command '${
def.get("name")
}' (source: ${fn.body.ctx.ref})`,
);
scriptEnv.registerCommand( scriptEnv.registerCommand(
{ {
name: def.get("name"), name: def.get("name"),
@ -67,10 +72,10 @@ function exposeDefinitions(
hide: def.get("hide"), hide: def.get("hide"),
} as CommandDef, } as CommandDef,
async (...args: any[]) => { async (...args: any[]) => {
const tl = new LuaEnv(); const tl = await buildThreadLocalEnv(system);
const sf = new LuaStackFrame(tl, null); const sf = new LuaStackFrame(tl, null);
try { try {
return await def.get(1).call(sf, ...args.map(jsToLuaValue)); return await fn.call(sf, ...args.map(jsToLuaValue));
} catch (e: any) { } catch (e: any) {
await handleLuaError(e, system); await handleLuaError(e, system);
} }
@ -88,13 +93,19 @@ function exposeDefinitions(
if (!def.get("event")) { if (!def.get("event")) {
throw new Error("Event is required"); throw new Error("Event is required");
} }
console.log("Subscribing to Lua event", def.get("event")); const fn = def.get(1);
console.log(
`[Lua] Subscribing to event '${
def.get("event")
}' (source: ${fn.body.ctx.ref})`,
);
scriptEnv.registerEventListener( scriptEnv.registerEventListener(
{ name: def.get("event") }, { name: def.get("event") },
async (...args: any[]) => { async (...args: any[]) => {
const sf = new LuaStackFrame(new LuaEnv(), null); const tl = await buildThreadLocalEnv(system);
const sf = new LuaStackFrame(tl, null);
try { try {
return await def.get(1).call(sf, ...args.map(jsToLuaValue)); return await fn.call(sf, ...args.map(jsToLuaValue));
} catch (e: any) { } catch (e: any) {
await handleLuaError(e, system); await handleLuaError(e, system);
} }
@ -104,6 +115,16 @@ function exposeDefinitions(
); );
} }
async function buildThreadLocalEnv(system: System<any>) {
const tl = new LuaEnv();
const currentPageMeta = await system.localSyscall(
"editor.getCurrentPageMeta",
[],
);
tl.setLocal("pageMeta", currentPageMeta);
return tl;
}
async function handleLuaError(e: any, system: System<any>) { async function handleLuaError(e: any, system: System<any>) {
console.error( console.error(
"Lua eval exception", "Lua eval exception",

View File

@ -79,7 +79,10 @@ async function renderExpressionDirective(
variables, variables,
functionMap, functionMap,
); );
return renderExpressionResult(result);
}
export function renderExpressionResult(result: any): string {
if ( if (
Array.isArray(result) && result.length > 0 && typeof result[0] === "object" Array.isArray(result) && result.length > 0 && typeof result[0] === "object"
) { ) {

View File

@ -1,4 +1,4 @@
import type { UploadFile } from "../types.ts"; import type { PageMeta, UploadFile } from "../types.ts";
import { syscall } from "../syscall.ts"; import { syscall } from "../syscall.ts";
import type { PageRef } from "../lib/page_ref.ts"; import type { PageRef } from "../lib/page_ref.ts";
import type { FilterOption } from "@silverbulletmd/silverbullet/type/client"; import type { FilterOption } from "@silverbulletmd/silverbullet/type/client";
@ -17,6 +17,14 @@ export function getCurrentPage(): Promise<string> {
return syscall("editor.getCurrentPage"); return syscall("editor.getCurrentPage");
} }
/**
* Returns the meta data of the page currently open in the editor.
* @returns the current page meta data
*/
export function getCurrentPageMeta(): Promise<PageMeta | undefined> {
return syscall("editor.getCurrentPageMeta");
}
/** /**
* Returns the full text of the currently open page * Returns the full text of the currently open page
*/ */

View File

@ -43,13 +43,13 @@ export function luaDirectivePlugin(client: Client) {
} }
const text = state.sliceDoc(node.from + 2, node.to - 1); const text = state.sliceDoc(node.from + 2, node.to - 1);
const currentPageMeta = client.ui.viewState.currentPageMeta;
widgets.push( widgets.push(
Decoration.widget({ Decoration.widget({
widget: new LuaWidget( widget: new LuaWidget(
node.from, node.from,
client, client,
`lua:${text}`, `lua:${text}:${currentPageMeta?.name}`,
text, text,
async (bodyText) => { async (bodyText) => {
try { try {
@ -58,14 +58,22 @@ export function luaDirectivePlugin(client: Client) {
(parsedLua.statements[0] as LuaFunctionCallStatement).call (parsedLua.statements[0] as LuaFunctionCallStatement).call
.args[0]; .args[0];
const sf = new LuaStackFrame(new LuaEnv(), expr.ctx); const tl = new LuaEnv();
return luaValueToJS( tl.setLocal("pageMeta", currentPageMeta);
const sf = new LuaStackFrame(tl, expr.ctx);
const threadLocalizedEnv = new LuaEnv(
client.clientSystem.spaceLuaEnv.env,
);
threadLocalizedEnv.setLocal("_CTX", tl);
const result = luaValueToJS(
await evalExpression( await evalExpression(
expr, expr,
client.clientSystem.spaceLuaEnv.env, threadLocalizedEnv,
sf, sf,
), ),
); );
// console.log("Result:", result);
return result;
} catch (e: any) { } catch (e: any) {
if (e instanceof LuaRuntimeError) { if (e instanceof LuaRuntimeError) {
if (e.sf.astCtx) { if (e.sf.astCtx) {

View File

@ -11,6 +11,7 @@ import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
import { renderToText } from "@silverbulletmd/silverbullet/lib/tree"; import { renderToText } from "@silverbulletmd/silverbullet/lib/tree";
import { activeWidgets } from "./markdown_widget.ts"; import { activeWidgets } from "./markdown_widget.ts";
import { attachWidgetEventHandlers } from "./widget_util.ts"; import { attachWidgetEventHandlers } from "./widget_util.ts";
import { renderExpressionResult } from "$common/template/render.ts";
export type LuaWidgetCallback = ( export type LuaWidgetCallback = (
bodyText: string, bodyText: string,
@ -97,10 +98,15 @@ export class LuaWidget extends WidgetType {
this.cacheKey, this.cacheKey,
{ height: div.clientHeight, html }, { height: div.clientHeight, html },
); );
} else if (widgetContent.markdown) { } else {
// If there is a markdown key, use it, otherwise render the objects as a markdown table
let mdContent = widgetContent.markdown;
if (!mdContent) {
mdContent = renderExpressionResult(widgetContent);
}
let mdTree = parse( let mdTree = parse(
extendedMarkdownLanguage, extendedMarkdownLanguage,
widgetContent.markdown!, mdContent,
); );
mdTree = await this.client.clientSystem.localSyscall( mdTree = await this.client.clientSystem.localSyscall(
"system.invokeFunction", "system.invokeFunction",
@ -187,8 +193,8 @@ export class LuaWidget extends WidgetType {
override eq(other: WidgetType): boolean { override eq(other: WidgetType): boolean {
return ( return (
other instanceof LuaWidget && other instanceof LuaWidget &&
other.bodyText === this.bodyText && other.cacheKey === this.cacheKey && other.bodyText === this.bodyText && other.cacheKey === this.cacheKey
this.from === other.from // && this.from === other.from
); );
} }
} }

View File

@ -12,7 +12,7 @@ import { EditorView } from "@codemirror/view";
import { getCM as vimGetCm, Vim } from "@replit/codemirror-vim"; import { getCM as vimGetCm, Vim } from "@replit/codemirror-vim";
import type { SysCallMapping } from "$lib/plugos/system.ts"; import type { SysCallMapping } from "$lib/plugos/system.ts";
import type { FilterOption } from "@silverbulletmd/silverbullet/type/client"; import type { FilterOption } from "@silverbulletmd/silverbullet/type/client";
import type { UploadFile } from "../../plug-api/types.ts"; import type { PageMeta, UploadFile } from "../../plug-api/types.ts";
import type { PageRef } from "@silverbulletmd/silverbullet/lib/page_ref"; import type { PageRef } from "@silverbulletmd/silverbullet/lib/page_ref";
import { openSearchPanel } from "@codemirror/search"; import { openSearchPanel } from "@codemirror/search";
import { diffAndPrepareChanges } from "../cm_util.ts"; import { diffAndPrepareChanges } from "../cm_util.ts";
@ -22,6 +22,9 @@ export function editorSyscalls(client: Client): SysCallMapping {
"editor.getCurrentPage": (): string => { "editor.getCurrentPage": (): string => {
return client.currentPage; return client.currentPage;
}, },
"editor.getCurrentPageMeta": (): PageMeta | undefined => {
return client.ui.viewState.currentPageMeta;
},
"editor.getText": () => { "editor.getText": () => {
return client.editorView.state.sliceDoc(); return client.editorView.state.sliceDoc();
}, },

View File

@ -81,7 +81,9 @@ You can listen to events using `define_event_listener`:
define_event_listener { define_event_listener {
event = "my-custom-event"; event = "my-custom-event";
function(e) function(e)
editor.flash_notification("Custom triggered: " .. e.data.name) editor.flash_notification("Custom triggered: "
.. e.data.name
.. " on page " .. _CTX.pageMeta.name)
end end
} }
``` ```
@ -106,6 +108,11 @@ Template:
Here's a greeting: {{greet_me("Pete")}} Here's a greeting: {{greet_me("Pete")}}
``` ```
# Thread locals
Theres a magic `_CTX` global variable available from which you can access useful context-specific value. Currently the following keys are available:
* `_CTX.pageMeta` contains a reference to the loaded page metadata (can be `nil` when not yet loaded)
# API # API
Lua APIs, which should be (roughly) implemented according to the Lua standard. Lua APIs, which should be (roughly) implemented according to the Lua standard.
* `print` * `print`