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()) {
const value = this.env.get(globalName);
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[]) => {
const sf = new LuaStackFrame(new LuaEnv(), value.body.ctx);
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,
LuaNativeJSFunction,
LuaStackFrame,
LuaTable,
luaValueToJS,
singleResult,
} from "./runtime.ts";
@ -11,9 +12,9 @@ import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts";
import { evalExpression, evalStatement } from "./eval.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 sf = new LuaStackFrame(e, node.ctx);
sf = sf || new LuaStackFrame(e, node.ctx);
return evalExpression(
node.call.args[0],
e,
@ -277,3 +278,149 @@ Deno.test("Statement evaluation", async () => {
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) {
return prefixValue.then(async (resolvedPrefix) => {
const args = await resolveVarargs();
return luaCall(resolvedPrefix, args, e.ctx, sf);
return luaCall(resolvedPrefix, args, e.ctx, sf.withCtx(e.ctx));
});
} else {
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: "__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: "__pow", nativeImplementation: (a, b) => a ** b },

View File

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

View File

@ -72,8 +72,8 @@ const tonumberFunction = new LuaBuiltinFunction((_sf, value: LuaValue) => {
return Number(value);
});
const errorFunction = new LuaBuiltinFunction((_sf, message: string) => {
throw new Error(message);
const errorFunction = new LuaBuiltinFunction((sf, message: string) => {
throw new LuaRuntimeError(message, sf);
});
const pcallFunction = new LuaBuiltinFunction(
@ -81,6 +81,9 @@ const pcallFunction = new LuaBuiltinFunction(
try {
return new LuaMultiRes([true, await luaCall(fn, args, sf.astCtx!, sf)]);
} catch (e: any) {
if (e instanceof LuaRuntimeError) {
return new LuaMultiRes([false, e.message]);
}
return new LuaMultiRes([false, e.message]);
}
},
@ -91,9 +94,10 @@ const xpcallFunction = new LuaBuiltinFunction(
try {
return new LuaMultiRes([true, await fn.call(sf, ...args)]);
} catch (e: any) {
const errorMsg = e instanceof LuaRuntimeError ? e.message : e.message;
return new LuaMultiRes([
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) => {
console.log(...args);
}),
stringify: new LuaBuiltinFunction((_sf, val) => JSON.stringify(val)),
// assignGlobal: new LuaBuiltinFunction((name: string, value: any) => {
// (globalThis as any)[name] = value;
// }),

View File

@ -56,7 +56,12 @@ function exposeDefinitions(
if (!def.get("name")) {
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(
{
name: def.get("name"),
@ -67,10 +72,10 @@ function exposeDefinitions(
hide: def.get("hide"),
} as CommandDef,
async (...args: any[]) => {
const tl = new LuaEnv();
const tl = await buildThreadLocalEnv(system);
const sf = new LuaStackFrame(tl, null);
try {
return await def.get(1).call(sf, ...args.map(jsToLuaValue));
return await fn.call(sf, ...args.map(jsToLuaValue));
} catch (e: any) {
await handleLuaError(e, system);
}
@ -88,13 +93,19 @@ function exposeDefinitions(
if (!def.get("event")) {
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(
{ name: def.get("event") },
async (...args: any[]) => {
const sf = new LuaStackFrame(new LuaEnv(), null);
const tl = await buildThreadLocalEnv(system);
const sf = new LuaStackFrame(tl, null);
try {
return await def.get(1).call(sf, ...args.map(jsToLuaValue));
return await fn.call(sf, ...args.map(jsToLuaValue));
} catch (e: any) {
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>) {
console.error(
"Lua eval exception",

View File

@ -79,7 +79,10 @@ async function renderExpressionDirective(
variables,
functionMap,
);
return renderExpressionResult(result);
}
export function renderExpressionResult(result: any): string {
if (
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 type { PageRef } from "../lib/page_ref.ts";
import type { FilterOption } from "@silverbulletmd/silverbullet/type/client";
@ -17,6 +17,14 @@ export function getCurrentPage(): Promise<string> {
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
*/

View File

@ -43,13 +43,13 @@ export function luaDirectivePlugin(client: Client) {
}
const text = state.sliceDoc(node.from + 2, node.to - 1);
const currentPageMeta = client.ui.viewState.currentPageMeta;
widgets.push(
Decoration.widget({
widget: new LuaWidget(
node.from,
client,
`lua:${text}`,
`lua:${text}:${currentPageMeta?.name}`,
text,
async (bodyText) => {
try {
@ -58,14 +58,22 @@ export function luaDirectivePlugin(client: Client) {
(parsedLua.statements[0] as LuaFunctionCallStatement).call
.args[0];
const sf = new LuaStackFrame(new LuaEnv(), expr.ctx);
return luaValueToJS(
const tl = new LuaEnv();
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(
expr,
client.clientSystem.spaceLuaEnv.env,
threadLocalizedEnv,
sf,
),
);
// console.log("Result:", result);
return result;
} catch (e: any) {
if (e instanceof LuaRuntimeError) {
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 { activeWidgets } from "./markdown_widget.ts";
import { attachWidgetEventHandlers } from "./widget_util.ts";
import { renderExpressionResult } from "$common/template/render.ts";
export type LuaWidgetCallback = (
bodyText: string,
@ -97,10 +98,15 @@ export class LuaWidget extends WidgetType {
this.cacheKey,
{ 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(
extendedMarkdownLanguage,
widgetContent.markdown!,
mdContent,
);
mdTree = await this.client.clientSystem.localSyscall(
"system.invokeFunction",
@ -187,8 +193,8 @@ export class LuaWidget extends WidgetType {
override eq(other: WidgetType): boolean {
return (
other instanceof LuaWidget &&
other.bodyText === this.bodyText && other.cacheKey === this.cacheKey &&
this.from === other.from
other.bodyText === this.bodyText && other.cacheKey === this.cacheKey
// && 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 type { SysCallMapping } from "$lib/plugos/system.ts";
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 { openSearchPanel } from "@codemirror/search";
import { diffAndPrepareChanges } from "../cm_util.ts";
@ -22,6 +22,9 @@ export function editorSyscalls(client: Client): SysCallMapping {
"editor.getCurrentPage": (): string => {
return client.currentPage;
},
"editor.getCurrentPageMeta": (): PageMeta | undefined => {
return client.ui.viewState.currentPageMeta;
},
"editor.getText": () => {
return client.editorView.state.sliceDoc();
},

View File

@ -81,7 +81,9 @@ You can listen to events using `define_event_listener`:
define_event_listener {
event = "my-custom-event";
function(e)
editor.flash_notification("Custom triggered: " .. e.data.name)
editor.flash_notification("Custom triggered: "
.. e.data.name
.. " on page " .. _CTX.pageMeta.name)
end
}
```
@ -106,6 +108,11 @@ Template:
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
Lua APIs, which should be (roughly) implemented according to the Lua standard.
* `print`