From 17ec0e41d1f83498014eba9a6754e8400ddf53f6 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Sun, 26 Jan 2025 08:07:58 +0100 Subject: [PATCH] Lua code complete --- plugs/core/Library/Std/Lua.md | 149 ++++++++++++++++++++++++++++++++++ web/hooks/slash_command.ts | 14 ++-- 2 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 plugs/core/Library/Std/Lua.md diff --git a/plugs/core/Library/Std/Lua.md b/plugs/core/Library/Std/Lua.md new file mode 100644 index 00000000..6e26c422 --- /dev/null +++ b/plugs/core/Library/Std/Lua.md @@ -0,0 +1,149 @@ +#meta + +Editor support for Lua, implemented in Lua. Of course. + +# Code complete support +```space-lua +local LUA_KEYWORDS = {"do", "if", "then", "for", "else", "end", "function", "local", "return"} + +-- Are we in a comment? +local function in_comment(line) + return string.find(line, "--") +end + +-- Are we in a string? +local function in_string(line) + local single_quotes = 0 + local double_quotes = 0 + local brackets = 0 + for i = 1, string.len(line) do + local c = line[i] + if c == "'" then + single_quotes = single_quotes + 1 + elseif c == '"' then + double_quotes = double_quotes + 1 + elseif c == "[" and line[i+1] == "[" then + brackets = brackets + 1 + elseif c == "]" and line[i-1] == "]" then + brackets = brackets - 1 + end + end + return single_quotes % 2 == 1 or double_quotes % 2 == 1 or brackets > 0 +end + +-- API code completion for Lua +-- Completes something.somethingelse APIs +event.listen { + name = "editor:complete", + run = function(e) + local parents = e.data.parentNodes + local found_space_lua = false + for _, parent in ipairs(parents) do + if string.startswith(parent, "FencedCode:space-lua") then + found_space_lua = true + end + end + if not found_space_lua then + return + end + local line_prefix = e.data.linePrefix + if in_comment(line_prefix) or in_string(line_prefix) then + return + end + local pos = e.data.pos + local propaccess_prefix = string.match_regex(line_prefix, "([a-zA-Z_0-9]+\\.)*([a-zA-Z_0-9]*)$") + if not propaccess_prefix or not propaccess_prefix[1] then + -- No propaccess prefix, so we can't complete + return + end + -- Split propaccess and traverse + local prop_parts = string.split(propaccess_prefix[1], ".") + local current_value = _CTX._GLOBAL + local failed = false + for i = 1, #prop_parts-1 do + local prop = prop_parts[i] + if current_value then + current_value = current_value[prop] + else + failed = true + end + end + local last_prop = prop_parts[#prop_parts] + if table.includes(LUA_KEYWORDS, last_prop) then + return + end + if not failed then + local options = {} + for key, val in pairs(current_value) do + if string.startswith(key, last_prop) and val then + if val.call then + -- We got a function + if val.body then + -- Function defined in Lua + table.insert(options, { + label = key .. "(" .. table.concat(val.body.parameters, ", ") ..")", + apply = key .. "(", + detail = "Lua function" + }) + else + -- Builtin + table.insert(options, { + label = key .. "()", + apply = key .. "(", + detail = "Lua built-in" + }) + end + else + -- Table + table.insert(options, { + label = key, + detail = "Lua table" + }) + end + end + end + if #options > 0 then + return { + from = pos - string.len(last_prop), + options = options + } + end + end + end +} +``` + +# Slash templates +Various useful slash templates. + +```space-lua +template.define_slash_command { + name = "function", + description = "Lua function", + only_contexts = {"FencedCode:space-lua"}, + template = template.new [==[function |^|() +end]==] +} + +template.define_slash_command { + name = "tpl", + description = "Lua template", + only_contexts = {"FencedCode:space-lua"}, + template = template.new "template.new[==[|^|]==]" +} + +template.define_slash_command { + name = "lua-query", + description = "Lua query", + only_contexts = {"FencedCode:space-lua", "LuaDirective"}, + template = template.new 'query[[from index.tag "|^|"]]' +} + +-- A query embedded in ${} +template.define_slash_command { + name = "query", + description = "Lua query", + except_contexts = {"FencedCode:space-lua", "LuaDirective"}, + template = function() return '${query[[from index.tag "|^|"]]}' end +} +``` diff --git a/web/hooks/slash_command.ts b/web/hooks/slash_command.ts index 2dfb638c..c8c0e72d 100644 --- a/web/hooks/slash_command.ts +++ b/web/hooks/slash_command.ts @@ -24,7 +24,7 @@ export type AppSlashCommand = { const slashCommandRegexp = /([^\w:]|^)\/[\w#\-]*/; export class SlashCommandHook implements Hook { - slashCommands = new Map(); + slashCommands: AppSlashCommand[] = []; private editor: Client; constructor(editor: Client, private commonSystem: CommonSystem) { @@ -38,7 +38,7 @@ export class SlashCommandHook implements Hook { buildAllCommands() { const system = this.commonSystem.system; - this.slashCommands.clear(); + this.slashCommands = []; for (const plug of system.loadedPlugs.values()) { for ( const [name, functionDef] of Object.entries( @@ -49,7 +49,7 @@ export class SlashCommandHook implements Hook { continue; } const cmd = functionDef.slashCommand; - this.slashCommands.set(cmd.name, { + this.slashCommands.push({ slashCommand: cmd, run: () => { return plug.invoke(name, [cmd]); @@ -59,11 +59,11 @@ export class SlashCommandHook implements Hook { } // Iterate over script defined slash commands for ( - const [name, command] of Object.entries( + const command of Object.values( this.commonSystem.scriptEnv.slashCommands, ) ) { - this.slashCommands.set(name, command); + this.slashCommands.push(command); } // Iterate over all shortcuts if (this.editor.config?.shortcuts) { @@ -71,7 +71,7 @@ export class SlashCommandHook implements Hook { for (const shortcut of this.editor.config.shortcuts) { if (shortcut.slashCommand) { const parsedCommand = parseCommand(shortcut.command); - this.slashCommands.set(shortcut.slashCommand, { + this.slashCommands.push({ slashCommand: { name: shortcut.slashCommand, description: parsedCommand.alias || parsedCommand.name, @@ -110,7 +110,7 @@ export class SlashCommandHook implements Hook { // Check if the slash command is available in the current context const parentNodes = this.editor.extractParentNodes(ctx.state, currentNode); - for (const def of this.slashCommands.values()) { + for (const def of this.slashCommands) { if ( def.slashCommand.onlyContexts && !def.slashCommand.onlyContexts.some( (context) => parentNodes.some((node) => node.startsWith(context)),