More Lua template work

pull/1224/head
Zef Hemel 2025-01-22 20:26:37 +01:00
parent 21c17ac737
commit fce78a22f8
22 changed files with 254 additions and 113 deletions

View File

@ -22,7 +22,7 @@ export abstract class CommonSystem {
// Hooks // Hooks
commandHook!: CommandHook; commandHook!: CommandHook;
slashCommandHook!: SlashCommandHook; slashCommandHook?: SlashCommandHook;
namespaceHook!: PlugNamespaceHook; namespaceHook!: PlugNamespaceHook;
codeWidgetHook!: CodeWidgetHook; codeWidgetHook!: CodeWidgetHook;
panelWidgetHook!: PanelWidgetHook; panelWidgetHook!: PanelWidgetHook;
@ -78,6 +78,10 @@ export abstract class CommonSystem {
this.eventHook.scriptEnvironment = this.scriptEnv; this.eventHook.scriptEnvironment = this.scriptEnv;
this.commandHook.throttledBuildAllCommands(); this.commandHook.throttledBuildAllCommands();
if (this.slashCommandHook) {
// Only on client
this.slashCommandHook.throttledBuildAllCommands();
}
} }
// Swap in the expanded function map // Swap in the expanded function map

View File

@ -1,4 +1,5 @@
import { parse } from "$common/space_lua/parse.ts"; import { parse, stripLuaComments } from "$common/space_lua/parse.ts";
import { assertEquals } from "@std/assert/equals";
Deno.test("Test Lua parser", () => { Deno.test("Test Lua parser", () => {
// Basic block test // Basic block test
@ -108,15 +109,21 @@ Deno.test("Test Lua parser", () => {
}); });
Deno.test("Test comment handling", () => { Deno.test("Test comment handling", () => {
parse(` const code = `
-- Single line comment -- Single line comment
--[[ Multi --[[ Multi
line line
comment ]] comment ]]
f([[ f([[
hello hello
-- yo -- yo
]])`); ]])`;
const code2 = stripLuaComments(code);
assertEquals(code2.length, code.length);
console.log(code2);
console.log(stripLuaComments(`e([==[
--- Hello
]==])`));
}); });
Deno.test("Test query parsing", () => { Deno.test("Test query parsing", () => {

View File

@ -635,76 +635,103 @@ function parseTableField(t: ParseTree, ctx: ASTCtx): LuaTableField {
throw new Error(`Unknown table field type: ${t.type}`); throw new Error(`Unknown table field type: ${t.type}`);
} }
} }
function stripLuaComments(s: string): string {
// Strips Lua comments (single-line and multi-line) and replaces them with equivalent length whitespace export function stripLuaComments(s: string): string {
let result = ""; let result = "";
let inString = false; let i = 0;
let inMultilineString = false;
let inComment = false;
let inMultilineComment = false;
for (let i = 0; i < s.length; i++) { while (i < s.length) {
// Handle string detection for single-line strings (to avoid stripping comments inside strings) // Check for long string
if ( if (s[i] === "[") {
s[i] === '"' && !inComment && !inMultilineComment && !inMultilineString let j = i + 1;
) { let equalsCount = 0;
inString = !inString; while (s[j] === "=") {
} equalsCount++;
j++;
}
if (s[j] === "[") {
// Found long string start
const openBracket = s.substring(i, j + 1);
const closeBracket = "]" + "=".repeat(equalsCount) + "]";
result += openBracket;
i = j + 1;
// Handle multi-line string literals (starting with "[[") // Find matching closing bracket
if ( const content = s.substring(i);
!inString && !inComment && !inMultilineComment && s[i] === "[" && const closeIndex = content.indexOf(closeBracket);
s[i + 1] === "[" if (closeIndex !== -1) {
) { // Copy string content verbatim, including any comment-like sequences
inMultilineString = true; result += content.substring(0, closeIndex) + closeBracket;
result += "[["; // Copy "[[" into result i += closeIndex + closeBracket.length;
i += 1; // Skip over "[[" continue;
continue; }
}
// Handle end of multi-line string literals (ending with "]]")
if (inMultilineString && s[i] === "]" && s[i + 1] === "]") {
inMultilineString = false;
result += "]]"; // Copy "]]" into result
i += 1; // Skip over "]]"
continue;
}
// Handle single-line comments (starting with "--")
if (
!inString && !inMultilineString && !inMultilineComment && s[i] === "-" &&
s[i + 1] === "-"
) {
if (s[i + 2] === "[" && s[i + 3] === "[") {
// Detect multi-line comment start "--[["
inMultilineComment = true;
i += 3; // Skip over "--[["
result += " "; // Add equivalent length spaces for "--[["
continue;
} else {
inComment = true;
} }
} }
// Handle end of single-line comment // Check for single quoted string
if (inComment && s[i] === "\n") { if (s[i] === '"' || s[i] === "'") {
inComment = false; const quote = s[i];
} result += quote;
i++;
// Handle multi-line comment ending "]]" while (i < s.length && s[i] !== quote) {
if (inMultilineComment && s[i] === "]" && s[i + 1] === "]") { if (s[i] === "\\") {
inMultilineComment = false; result += s[i] + s[i + 1];
i += 1; // Skip over "]]" i += 2;
result += " "; // Add equivalent length spaces for "]]" } else {
result += s[i];
i++;
}
}
if (i < s.length) {
result += s[i]; // closing quote
i++;
}
continue; continue;
} }
// Replace comment content with spaces, or copy original content if not in comment or multi-line string // Check for comments
if (inComment || inMultilineComment) { if (s[i] === "-" && s[i + 1] === "-") {
result += " "; // Replace comment characters with spaces // Replace the -- with spaces
} else { result += " ";
result += s[i]; i += 2;
// Check for long comment
if (s[i] === "[") {
let j = i + 1;
let equalsCount = 0;
while (s[j] === "=") {
equalsCount++;
j++;
}
if (s[j] === "[") {
// Found long comment start
const closeBracket = "]" + "=".repeat(equalsCount) + "]";
// Replace opening bracket with spaces
result += " ".repeat(j - i + 1);
i = j + 1;
// Find matching closing bracket
const content = s.substring(i);
const closeIndex = content.indexOf(closeBracket);
if (closeIndex !== -1) {
// Replace comment content and closing bracket with spaces
result += " ".repeat(closeIndex) + " ".repeat(closeBracket.length);
i += closeIndex + closeBracket.length;
continue;
}
}
}
// Single line comment - replace rest of line with spaces
while (i < s.length && s[i] !== "\n") {
result += " ";
i++;
}
continue;
} }
result += s[i];
i++;
} }
return result; return result;

View File

@ -80,6 +80,17 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
} }
return keys; return keys;
} }
toJSON(omitKeys: string[] = []): Record<string, any> {
const result: Record<string, any> = {};
for (const key of this.keys()) {
if (omitKeys.includes(key)) {
continue;
}
result[key] = luaValueToJS(this.get(key));
}
return result;
}
} }
export class LuaStackFrame { export class LuaStackFrame {

View File

@ -39,13 +39,24 @@ function exposeSyscalls(env: LuaEnv, system: System<any>) {
} }
} }
export function buildThreadLocalEnv(_system: System<any>, globalEnv: LuaEnv) { export async function buildThreadLocalEnv(
system: System<any>,
globalEnv: LuaEnv,
) {
const tl = new LuaEnv(); const tl = new LuaEnv();
// const currentPageMeta = await system.localSyscall( if (system.registeredSyscalls.has("editor.getCurrentPageMeta")) {
// "editor.getCurrentPageMeta", const currentPageMeta = await system.localSyscall(
// [], "editor.getCurrentPageMeta",
// ); [],
// tl.setLocal("pageMeta", currentPageMeta); );
if (currentPageMeta) {
tl.setLocal("currentPage", currentPageMeta);
} else {
tl.setLocal("currentPage", {
name: await system.localSyscall("editor.getCurrentPage", []),
});
}
}
tl.setLocal("_GLOBAL", globalEnv); tl.setLocal("_GLOBAL", globalEnv);
return Promise.resolve(tl); return Promise.resolve(tl);
} }

View File

@ -1,9 +1,10 @@
import type { System } from "../lib/plugos/system.ts"; import type { System } from "../lib/plugos/system.ts";
import type { ParseTree } from "../plug-api/lib/tree.ts"; import type { ParseTree } from "../plug-api/lib/tree.ts";
import type { ScriptObject } from "../plugs/index/script.ts"; import type { ScriptObject } from "../plugs/index/script.ts";
import type { AppCommand, CommandDef } from "$lib/command.ts"; import type { AppCommand, CommandDef, SlashCommand } from "$lib/command.ts";
import { Intl, Temporal, toTemporalInstant } from "@js-temporal/polyfill"; import { Intl, Temporal, toTemporalInstant } from "@js-temporal/polyfill";
import * as syscalls from "@silverbulletmd/silverbullet/syscalls"; import * as syscalls from "@silverbulletmd/silverbullet/syscalls";
import type { SlashCommandDef } from "$lib/manifest.ts";
// @ts-ignore: Temporal polyfill // @ts-ignore: Temporal polyfill
Date.prototype.toTemporalInstant = toTemporalInstant; Date.prototype.toTemporalInstant = toTemporalInstant;
@ -32,6 +33,7 @@ type AttributeExtractorCallback = (
export class ScriptEnvironment { export class ScriptEnvironment {
functions: Record<string, (...args: any[]) => any> = {}; functions: Record<string, (...args: any[]) => any> = {};
commands: Record<string, AppCommand> = {}; commands: Record<string, AppCommand> = {};
slashCommands: Record<string, SlashCommand> = {};
attributeExtractors: Record<string, AttributeExtractorCallback[]> = {}; attributeExtractors: Record<string, AttributeExtractorCallback[]> = {};
eventHandlers: Record<string, ((...args: any[]) => any)[]> = {}; eventHandlers: Record<string, ((...args: any[]) => any)[]> = {};
@ -71,6 +73,16 @@ export class ScriptEnvironment {
}; };
} }
registerSlashCommand(
def: SlashCommandDef,
fn: (...args: any[]) => any,
) {
this.slashCommands[def.name] = {
slashCommand: def,
run: fn,
};
}
registerAttributeExtractor( registerAttributeExtractor(
def: AttributeExtractorDef, def: AttributeExtractorDef,
callback: AttributeExtractorCallback, callback: AttributeExtractorCallback,

View File

@ -46,5 +46,27 @@ export function commandSyscalls(
}, },
); );
}, },
"slash_command.define": (
_ctx,
def: CallbackCommandDef,
) => {
commonSystem.scriptEnv.registerSlashCommand(
def,
async (...args: any[]) => {
const tl = await buildThreadLocalEnv(
commonSystem.system,
commonSystem.spaceLuaEnv.env,
);
const sf = new LuaStackFrame(tl, null);
try {
return luaValueToJS(
await luaCall(def.run, args.map(jsToLuaValue), sf),
);
} catch (e: any) {
await handleLuaError(e, commonSystem.system);
}
},
);
},
}; };
} }

View File

@ -10,7 +10,7 @@ import {
type LuaQueryCollection, type LuaQueryCollection,
} from "$common/space_lua/query_collection.ts"; } from "$common/space_lua/query_collection.ts";
import { import {
type LuaEnv, LuaEnv,
LuaRuntimeError, LuaRuntimeError,
type LuaStackFrame, type LuaStackFrame,
luaValueToJS, luaValueToJS,
@ -90,7 +90,12 @@ export function indexSyscalls(commonSystem: CommonSystem): SysCallMapping {
const scopedVariables: Record<string, any> = {}; const scopedVariables: Record<string, any> = {};
for (const v of localVars) { for (const v of localVars) {
try { try {
const jsonValue = await luaValueToJS(env.get(v)); let value = env.get(v);
if (value instanceof LuaEnv) {
// We don't want to include the global environment in the serialized value
value = value.toJSON(["_GLOBAL"]);
}
const jsonValue = await luaValueToJS(value);
// Ensure this is JSON serializable // Ensure this is JSON serializable
JSON.stringify(jsonValue); JSON.stringify(jsonValue);
scopedVariables[v] = jsonValue; scopedVariables[v] = jsonValue;

View File

@ -1,3 +1,5 @@
import type { SlashCommandDef } from "$lib/manifest.ts";
export type CommandDef = { export type CommandDef = {
name: string; name: string;
@ -19,6 +21,11 @@ export type AppCommand = {
run: (args?: any[]) => Promise<void>; run: (args?: any[]) => Promise<void>;
}; };
export type SlashCommand = {
slashCommand: SlashCommandDef;
run: (args?: any[]) => Promise<void>;
};
export type CommandHookEvents = { export type CommandHookEvents = {
commandsUpdated(commandMap: Map<string, AppCommand>): void; commandsUpdated(commandMap: Map<string, AppCommand>): void;
}; };

View File

@ -3,7 +3,7 @@
Config library for defining and getting config values Config library for defining and getting config values
```space-lua ```space-lua
-- priority: 100 -- priority: 10
config = {} config = {}
local config_values = {} local config_values = {}

View File

@ -3,9 +3,11 @@
Implements useful template functions Implements useful template functions
```space-lua ```space-lua
-- priority: 100 -- priority: 10
-- Template library for working with templates and iterables -- Template API root table
template = {} template = {}
-- Template storage table
templates = {}
-- Iterates over a table/array and applies a function to each element, -- Iterates over a table/array and applies a function to each element,
-- concatenating the results -- concatenating the results
@ -31,8 +33,4 @@ function template.new(template_str)
return space_lua.interpolate(template_str, obj) return space_lua.interpolate(template_str, obj)
end end
end end
print("template loaded!!")
``` ```

View File

@ -2,7 +2,6 @@ import type { IndexTreeEvent } from "../../plug-api/types.ts";
import { collectNodesOfType, findNodeOfType } from "../../plug-api/lib/tree.ts"; import { collectNodesOfType, findNodeOfType } from "../../plug-api/lib/tree.ts";
import type { ObjectValue } from "../../plug-api/types.ts"; import type { ObjectValue } from "../../plug-api/types.ts";
import { indexObjects } from "./api.ts"; import { indexObjects } from "./api.ts";
import { space } from "@silverbulletmd/silverbullet/syscalls";
export type ScriptObject = ObjectValue<{ export type ScriptObject = ObjectValue<{
script: string; script: string;
priority?: number; priority?: number;

View File

@ -55,14 +55,13 @@ export async function insertSnippetTemplate(
const config = await system.getSpaceConfig(); const config = await system.getSpaceConfig();
const templateText = await space.readPage(slashCompletion.templatePage); const templateText = await space.readPage(slashCompletion.templatePage);
let { renderedFrontmatter, text: replacementText, frontmatter } = const { renderedFrontmatter, text: replacementText, frontmatter } =
await renderTemplate( await renderTemplate(
templateText, templateText,
pageObject, pageObject,
{ page: pageObject, config }, { page: pageObject, config },
); );
const snippetTemplate: SnippetConfig = frontmatter.hooks.snippet; const snippetTemplate: SnippetConfig = frontmatter.hooks.snippet;
let cursorPos = await editor.getCursor(); let cursorPos = await editor.getCursor();
if (renderedFrontmatter) { if (renderedFrontmatter) {
@ -108,8 +107,21 @@ export async function insertSnippetTemplate(
cursorPos = await editor.getCursor(); cursorPos = await editor.getCursor();
} }
if (snippetTemplate.insertAt) { await applySnippetTemplate(replacementText, snippetTemplate);
switch (snippetTemplate.insertAt) { }
export async function applySnippetTemplate(
templateText: string,
config: {
insertAt?: string;
match?: string;
matchRegex?: string;
},
) {
let cursorPos = await editor.getCursor();
if (config.insertAt) {
switch (config.insertAt) {
case "page-start": case "page-start":
await editor.moveCursor(0); await editor.moveCursor(0);
break; break;
@ -141,12 +153,12 @@ export async function insertSnippetTemplate(
cursorPos = await editor.getCursor(); cursorPos = await editor.getCursor();
if (snippetTemplate.match || snippetTemplate.matchRegex) { if (config.match || config.matchRegex) {
const pageText = await editor.getText(); const pageText = await editor.getText();
// Regex matching mode // Regex matching mode
const matchRegex = new RegExp( const matchRegex = new RegExp(
(snippetTemplate.match || snippetTemplate.matchRegex)!, (config.match || config.matchRegex)!,
); );
let startOfLine = cursorPos; let startOfLine = cursorPos;
@ -158,7 +170,7 @@ export async function insertSnippetTemplate(
endOfLine++; endOfLine++;
} }
let currentLine = pageText.slice(startOfLine, endOfLine); let currentLine = pageText.slice(startOfLine, endOfLine);
const caretParts = replacementText.split("|^|"); const caretParts = templateText.split("|^|");
const emptyLine = !currentLine; const emptyLine = !currentLine;
currentLine = currentLine.replace(matchRegex, caretParts[0]); currentLine = currentLine.replace(matchRegex, caretParts[0]);
@ -190,9 +202,9 @@ export async function insertSnippetTemplate(
selection: newSelection, selection: newSelection,
}); });
} else { } else {
const carretPos = replacementText.indexOf("|^|"); const carretPos = templateText.indexOf("|^|");
replacementText = replacementText.replace("|^|", ""); templateText = templateText.replace("|^|", "");
await editor.insertAtCursor(replacementText); await editor.insertAtCursor(templateText);
if (carretPos !== -1) { if (carretPos !== -1) {
await editor.moveCursor(cursorPos + carretPos); await editor.moveCursor(cursorPos + carretPos);
} }

View File

@ -11,6 +11,10 @@ functions:
instantiatePageTemplate: instantiatePageTemplate:
path: page.ts:instantiatePageTemplate path: page.ts:instantiatePageTemplate
applySnippetTemplate:
path: snippet.ts:applySnippetTemplate
env: client
# Indexing # Indexing
indexTemplate: indexTemplate:
path: index.ts:indexTemplate path: index.ts:indexTemplate

View File

@ -273,9 +273,7 @@ export class Client implements ConfigContainer {
type: "config-loaded", type: "config-loaded",
config: this.config, config: this.config,
}); });
this.clientSystem.slashCommandHook.buildAllCommands( this.clientSystem.slashCommandHook!.buildAllCommands();
this.clientSystem.system,
);
this.eventHook.dispatchEvent("config:loaded", this.config); this.eventHook.dispatchEvent("config:loaded", this.config);
} }

View File

@ -125,7 +125,7 @@ export class ClientSystem extends CommonSystem {
this.system.addHook(this.commandHook); this.system.addHook(this.commandHook);
// Slash command hook // Slash command hook
this.slashCommandHook = new SlashCommandHook(this.client); this.slashCommandHook = new SlashCommandHook(this.client, this);
this.system.addHook(this.slashCommandHook); this.system.addHook(this.slashCommandHook);
this.eventHook.addLocalListener( this.eventHook.addLocalListener(
@ -147,7 +147,7 @@ export class ClientSystem extends CommonSystem {
init() { init() {
// Slash command hook // Slash command hook
this.slashCommandHook = new SlashCommandHook(this.client); this.slashCommandHook = new SlashCommandHook(this.client, this);
this.system.addHook(this.slashCommandHook); this.system.addHook(this.slashCommandHook);
// Syscalls available to all plugs // Syscalls available to all plugs

View File

@ -56,7 +56,7 @@ class InlineContentWidget extends WidgetType {
return div; return div;
} }
const url = encodeURIComponent(this.url) const url = encodeURIComponent(this.url);
if (mimeType.startsWith("image/")) { if (mimeType.startsWith("image/")) {
const img = document.createElement("img"); const img = document.createElement("img");

View File

@ -59,7 +59,11 @@ export function luaDirectivePlugin(client: Client) {
.args[0]; .args[0];
const tl = new LuaEnv(); const tl = new LuaEnv();
tl.setLocal("pageMeta", currentPageMeta); tl.setLocal(
"currentPage",
currentPageMeta ||
{ name: client.ui.viewState.currentPage },
);
tl.setLocal("_GLOBAL", client.clientSystem.spaceLuaEnv.env); tl.setLocal("_GLOBAL", client.clientSystem.spaceLuaEnv.env);
const sf = new LuaStackFrame(tl, expr.ctx); const sf = new LuaStackFrame(tl, expr.ctx);
const threadLocalizedEnv = new LuaEnv( const threadLocalizedEnv = new LuaEnv(

View File

@ -104,7 +104,10 @@ export function TopBar({
<div className="sb-actions"> <div className="sb-actions">
{progressPerc !== undefined && {progressPerc !== undefined &&
( (
<div className="progress-wrapper" title={`Sync Progress: ${progressPerc}%`}> <div
className="progress-wrapper"
title={`Sync Progress: ${progressPerc}%`}
>
<div <div
className="progress-bar" className="progress-bar"
style={`background: radial-gradient(closest-side, var(--top-background-color) 79%, transparent 80% 100%), conic-gradient(var(--button-color) ${progressPerc}%, var(--button-background-color) 0);`} style={`background: radial-gradient(closest-side, var(--top-background-color) 79%, transparent 80% 100%), conic-gradient(var(--button-color) ${progressPerc}%, var(--button-background-color) 0);`}

View File

@ -110,7 +110,7 @@ export function createEditorState(
autocompletion({ autocompletion({
override: [ override: [
client.editorComplete.bind(client), client.editorComplete.bind(client),
client.clientSystem.slashCommandHook.slashCommandCompleter.bind( client.clientSystem.slashCommandHook!.slashCommandCompleter.bind(
client.clientSystem.slashCommandHook, client.clientSystem.slashCommandHook,
), ),
], ],

View File

@ -11,9 +11,10 @@ import type {
SlashCompletionOption, SlashCompletionOption,
SlashCompletions, SlashCompletions,
} from "../../plug-api/types.ts"; } from "../../plug-api/types.ts";
import { safeRun } from "$lib/async.ts"; import { safeRun, throttle } from "$lib/async.ts";
import type { SlashCommandDef, SlashCommandHookT } from "$lib/manifest.ts"; import type { SlashCommandDef, SlashCommandHookT } from "$lib/manifest.ts";
import { parseCommand } from "$common/command.ts"; import { parseCommand } from "$common/command.ts";
import type { CommonSystem } from "$common/common_system.ts";
export type AppSlashCommand = { export type AppSlashCommand = {
slashCommand: SlashCommandDef; slashCommand: SlashCommandDef;
@ -26,11 +27,17 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
slashCommands = new Map<string, AppSlashCommand>(); slashCommands = new Map<string, AppSlashCommand>();
private editor: Client; private editor: Client;
constructor(editor: Client) { constructor(editor: Client, private commonSystem: CommonSystem) {
this.editor = editor; this.editor = editor;
} }
buildAllCommands(system: System<SlashCommandHookT>) { throttledBuildAllCommands = throttle(() => {
this.buildAllCommands();
}, 200);
buildAllCommands() {
const system = this.commonSystem.system;
this.slashCommands.clear(); this.slashCommands.clear();
for (const plug of system.loadedPlugs.values()) { for (const plug of system.loadedPlugs.values()) {
for ( for (
@ -50,6 +57,15 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
}); });
} }
} }
// Iterate over script defined slash commands
for (
const [name, command] of Object.entries(
this.commonSystem.scriptEnv.slashCommands,
)
) {
this.slashCommands.set(name, command);
}
// Iterate over all shortcuts
if (this.editor.config?.shortcuts) { if (this.editor.config?.shortcuts) {
// Add slash commands for shortcuts that configure them // Add slash commands for shortcuts that configure them
for (const shortcut of this.editor.config.shortcuts) { for (const shortcut of this.editor.config.shortcuts) {
@ -161,10 +177,10 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
} }
apply(system: System<SlashCommandHookT>): void { apply(system: System<SlashCommandHookT>): void {
this.buildAllCommands(system); this.buildAllCommands();
system.on({ system.on({
plugLoaded: () => { plugLoaded: () => {
this.buildAllCommands(system); this.buildAllCommands();
}, },
}); });
} }

View File

@ -137,7 +137,8 @@ Space Lua currently introduces a few new features on top core Lua:
## Thread locals ## Thread locals
Theres a magic `_CTX` global variable available from which you can access useful context-specific values. Currently the following keys are available: Theres a magic `_CTX` global variable available from which you can access useful context-specific values. Currently the following keys are available:
* `_CTX.GLOBAL` providing access to the global scope * `_CTX.currentPage` providing access (in the client only) to the currently open page (PageMeta object)
* `_CTX._GLOBAL` providing access to the global scope
# 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.