#meta Basic editor support for Space Lua, implemented in Space Lua. Features: * Basic code completion for globals, e.g. `string.gm` will be completed * Navigation to global symbols implemented in Lua by Cmd-clicking (Mac) or Ctrl-clicking (other OSes) # Code complete support ```space-lua -- priority: 10 local LUA_KEYWORDS = {"do", "if", "then", "for", "else", "end", "function", "local", "return", "in"} -- Are we in a comment? local function inComment(line) return string.find(line, "%-%-") end -- Are we in a string? local function inString(line) local singleQuotes = 0 local doubleQuotes = 0 local brackets = 0 for i = 1, string.len(line) do local c = line[i] if c == "'" then singleQuotes = singleQuotes + 1 elseif c == '"' then doubleQuotes = doubleQuotes + 1 elseif c == "[" and line[i+1] == "[" and line:sub(i-5, i) != "query[" then brackets = brackets + 1 elseif c == "]" and line[i-1] == "]" then brackets = brackets - 1 end end return singleQuotes % 2 == 1 or doubleQuotes % 2 == 1 or brackets > 0 end -- Converts (some) JSON schema to dummy Lua values for code complete to use local function schemaToDummyValue(schema) if schema.type == "string" then return "" elseif schema.type == "number" then return 0 elseif schema.type == "boolean" then return true elseif schema.type == "array" then return {} elseif schema.type == "object" then local t = {} for k, v in pairs(schema.properties) do t[k] = schemaToDummyValue(v) end return t else return nil end end -- Implements extremely simplistic global variable augmentation by scanning for local variables (declared via `local` and `from`) local function augmentGlobal(localCode) local vars = {} -- Copy over all globals for k, v in pairs(_G) do vars[k] = v end -- Then approximate specific local tag-based object declarations in queries and add them -- matches `tags.something`, `index.tag "something"` and `index.tag("something")` patterns only for m in string.matchRegexAll(localCode, "(?:from|local)\\s+(\\w+)\\s+=\\s*(?:index\\.objects\\s*\"(\\w+)\"|index\\.objects\\s*\\(\"(\\w+)\"\\)|tags\\.(\\w+))") do local tag for i, t in ipairs(m) do if i > 2 and t then tag = t end end -- Do we have a schema for this tag? local schema = config.get({"tags", tag, "schema"}) if schema then -- Schema exists, add to vars vars[m[2]] = schemaToDummyValue(schema) end end return vars end -- API code completion for Lua -- Completes a.b-style property accesses event.listen { name = "editor:complete", run = function(e) local parents = e.data.parentNodes local luaCode = nil for _, parent in ipairs(parents) do if string.startsWith(parent, "FencedCode:space-lua") then luaCode = string.sub(parent, #"FencedCode:space-lua"+1) elseif string.startsWith(parent, "LuaDirective:") then luaCode = string.sub(parent, #"LuaDirective:"+1) end end -- Not in a Lua context if not luaCode then return end local linePrefix = e.data.linePrefix if inComment(linePrefix) or inString(linePrefix) then return end local pos = e.data.pos local propaccessPrefix = string.matchRegex(linePrefix, "([a-zA-Z_0-9]+\\.)*([a-zA-Z_0-9]*)$") if not propaccessPrefix or propaccessPrefix[1] == "" then -- No propaccess prefix, so we can't complete return end -- Split propaccess and traverse local propParts = string.split(propaccessPrefix[1], ".") local currentValue = augmentGlobal(luaCode) local failed = false for i = 1, #propParts-1 do local prop = propParts[i] if currentValue then currentValue = currentValue[prop] else failed = true end end if failed then return end local lastProp = propParts[#propParts] if table.includes(LUA_KEYWORDS, lastProp) then return end local options = {} if not currentValue then return end for key, val in pairs(currentValue) do if string.startsWith(key, lastProp) and val then if type(val) == "function" 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 = "function" }) else -- Builtin table.insert(options, { label = key .. "()", apply = key, detail = "built-in" }) end else table.insert(options, { label = key, detail = type(val), }) end end end if #options > 0 then return { from = pos - string.len(lastProp), options = options } end end } ``` # Navigation Ctrl/Cmd-click navigation to Lua function definition. ```space-lua -- priority: 10 local function inLuaContext(parentNodes) for _, node in ipairs(parentNodes) do if node == "LuaDirective" or node:startsWith("FencedCode:space-lua") then return true end end return false end local function notDefinedinLua() return editor.flashNotification( "Cannot navigate to definition; not defined in Lua." ) end event.listen { name = "page:click", run = function(e) -- Accept either Cmd-click (Mac) or Ctrl-click (Linux) if not (e.data.metaKey or e.data.ctrlKey) then return end if not inLuaContext(e.data.parentNodes) then return notDefinedinLua() end local pos = e.data.pos local text = editor.getText() -- Find start pos local startPos = pos while string.match(text[startPos], "[a-zA-Z0-9._]") do startPos = startPos - 1 if startPos <= 0 then return end end -- Find end pos local endPos = pos while string.match(text[endPos], "[a-zA-Z0-9_]") do endPos = endPos + 1 if startPos >= #text then return end end local callText = text:sub(startPos+1, endPos-1) local propParts = callText:split(".") local currentValue = _G for i = 1, #propParts do local prop = propParts[i] if currentValue then currentValue = currentValue[prop] else return end end -- Check if this is a Lua-defined API if currentValue and currentValue.body and currentValue.body.ctx then local ctx = currentValue.body.ctx -- Parse out the position in the doc local refBits = ctx.ref:split("@") -- Navigate there editor.navigate(refBits[1] .. "@" .. (tonumber(refBits[2]) + ctx.from + #"```space-lua\n") ) else return notDefinedinLua() end end } ```