273 lines
7.6 KiB
Markdown
273 lines
7.6 KiB
Markdown
#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
|
|
-- m[1] = full match, m[2] = variable name, m[3..5] = tag from each alternative
|
|
local tag = m[3] or m[4] or m[5]
|
|
-- 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)
|
|
if not parentNodes then
|
|
return false
|
|
end
|
|
for _, node in ipairs(parentNodes) do
|
|
if node == "LuaDirective"
|
|
or node:startsWith("FencedCode:space-lua") then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Clicks on actual links (wiki links, markdown links, URLs, hashtags, etc.)
|
|
-- must be left to the regular navigation handler so they can be opened
|
|
-- (in a new window/tab on Cmd/Ctrl-click), even inside a Lua block.
|
|
local function onLink(parentNodes)
|
|
if not parentNodes then
|
|
return false
|
|
end
|
|
for _, node in ipairs(parentNodes) do
|
|
if node == "WikiLink" or node == "Link" or node == "Image"
|
|
or node == "Autolink" or node == "NakedURL" or node == "Hashtag"
|
|
or node == "FootnoteRef" 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
|
|
-- Don't capture clicks on links; let the normal navigation handler deal
|
|
-- with them.
|
|
if onLink(e.data.parentNodes) then
|
|
return
|
|
end
|
|
-- Only attempt to navigate to a definition from within Lua code.
|
|
if not inLuaContext(e.data.parentNodes) then
|
|
return
|
|
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
|
|
}
|
|
```
|