Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a script to generate autocomplete files using EmmyLua annotations #2530

Closed
wants to merge 11 commits into from
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ build/docs.sqlite: build/docs.json
build/docs.json: build
scripts/docs/bin/build_docs.py -o build/ --json $(DOCS_SEARCH_DIRS)

build/stubs: build/docs.json
scripts/stubs.py

build:
mkdir -p build

Expand Down
165 changes: 165 additions & 0 deletions scripts/stubs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Hammerspoon autocompletion stubs using EmmyLua annotations for lua lsp servers"""
folke marked this conversation as resolved.
Show resolved Hide resolved

import codecs
import json
import os
import re
import sys
folke marked this conversation as resolved.
Show resolved Hide resolved

typeOverrides = {
"app": "hs.application",
"hsminwebtable": "hs.httpserver.hsminweb",
"notificationobject": "hs.notify",
"point": "hs.geometry",
"rect": "hs.geometry",
"hs.geometry rect": "hs.geometry",
"size": "hs.geometry",
}


def parseType(module, expr, depth=1):
t = expr.lower()
if t in typeOverrides:
return typeOverrides[t]
m = re.match("^[`'\"]?(hs\.[\w.]+)[`'\"]?([\s+\-\s*]?object)?$", t)
folke marked this conversation as resolved.
Show resolved Hide resolved
if m:
return m.group(1)
m = re.match("^list of [`'\"]?(hs\.[\w.]+)[`'\"]?(\s+objects)?$", t)
folke marked this conversation as resolved.
Show resolved Hide resolved
if m:
return m.group(1) + "[]"
elif re.match("^true|false|bool(ean)?$", t):
t = "boolean"
elif t == "string":
t = "string"
elif re.match("number|integer|float", t):
t = "number"
elif re.match("array|table|list|object", t):
t = "table"
elif re.match("none|nil|null|nothing", t):
return None
elif t == "self" or re.match(
"^" + re.escape(module["name"].split(".")[-1].lower()) + "\s*(object)?$", t
folke marked this conversation as resolved.
Show resolved Hide resolved
):
t = module["name"]
else:
# when multiple types are possible, parse the first type
parts = re.split("(\s*[,\|]\s*|\s+or\s+)", t)
folke marked this conversation as resolved.
Show resolved Hide resolved
if len(parts) > 1:
first = parseType(module, parts[0], depth + 1)
if first:
return first
# if depth == 1:
# print((expr, t))
return None
return t


def parseSignature(module, expr):
parts = re.split("\s*-+>\s*", expr, 2)
folke marked this conversation as resolved.
Show resolved Hide resolved
if len(parts) == 2:
return (parts[0], parseType(module, parts[1]))
return (parts[0], None)


def processFunction(f, module, el, returnType=False):
left, type = parseSignature(module, el["signature"])
m = re.match("^(.*)\((.*)\)$", left)
folke marked this conversation as resolved.
Show resolved Hide resolved
if m:
name = m.group(1)
params = m.group(2).strip()
params = re.sub("[\[\]\{\}\(\)]+", "", params)
folke marked this conversation as resolved.
Show resolved Hide resolved
params = re.sub("(\s*\|\s*|\s+or\s+)", "_or_", params)
folke marked this conversation as resolved.
Show resolved Hide resolved
params = re.sub("\s*,\s*", ",", params)
folke marked this conversation as resolved.
Show resolved Hide resolved
params = ", ".join(
map(
lambda x: re.sub("^(end|function|false)$", "_\\1", x), params.split(",")
folke marked this conversation as resolved.
Show resolved Hide resolved
)
)
addDef = (name + "(" + params + ")").replace(" ", "") != left.replace(" ", "")
folke marked this conversation as resolved.
Show resolved Hide resolved
ret = doc(el, addDef)
if returnType:
ret += "---@return " + returnType + "\n"
elif type:
ret += "---@return " + type + "\n"
ret += "function " + name + "(" + params + ") end\n\n"
f.write(ret)
else:
print(
"Warning: invalid function definition:\n " + el["signature"] + "\n " + left
folke marked this conversation as resolved.
Show resolved Hide resolved
)


def processVar(f, module, var):
ret = doc(var)
left, type = parseSignature(module, var["signature"])

if left.endswith("[]"):
if type:
if not type.endswith("[]"):
type += "[]"
ret += "---@type " + type + "\n"
ret += left[0:-2] + " = {}\n"
else:
if type:
ret += "---@type " + type + "\n"
ret += left + " = nil\n"
f.write(ret + "\n")


def doc(el, addDef=False):
ret = ""
if addDef and "def" in el:
parts = re.split("\s*-+>\s*", el["def"], 2)
folke marked this conversation as resolved.
Show resolved Hide resolved
ret += "---`" + parts[0] + "`\n---\n"
ret += "---" + "---".join(el["doc"].strip().splitlines(True)) + "\n"
return ret


def processModule(dir, module):
name = module["name"]

f = codecs.open(dir + "/" + name + ".lua", "w", "utf-8")

if name == "hs":
f.write("function __IGNORE(...) end\n")
f.write("--- global variable that contains loaded spoons\nspoon = {}\n")
folke marked this conversation as resolved.
Show resolved Hide resolved

ret = doc(module)
ret += "---@class " + name + "\n"
ret += name + " = {}\n"
f.write(ret + "\n")

for function in module["Function"]:
processFunction(f, module, function)
for function in module["Method"]:
processFunction(f, module, function)
for function in module["Constructor"]:
processFunction(f, module, function, module["name"])
for function in module["Variable"]:
processVar(f, module, function)
for function in module["Constant"]:
processVar(f, module, function)

f.write("\n\n")
f.close()


def main():
target = "build/stubs"
if not os.path.exists(target):
os.mkdir(target)

with open("build/docs.json") as json_file:
data = json.load(json_file)
c = 0
folke marked this conversation as resolved.
Show resolved Hide resolved
for m in data:
if m["type"] == "Module":
processModule(target, m)
else:
raise Exception("Unknown type " + m["type"])


if __name__ == "__main__":
main()