6.7 KiB
6.7 KiB
#meta
Basic editor support for Space Lua, implemented in Space Lua.
Features:
- Basic code completion for globals, e.g.
string.gmwill be completed - Navigation to global symbols implemented in Lua by Cmd-clicking (Mac) or Ctrl-clicking (other OSes)
Code complete support
-- 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\\.tag\\s*\"(\\w+)\"|index\\.tag\\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.
-- 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
}