diff --git a/.gitignore b/.gitignore index bb0ce15..003a793 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /result /lua*.html /build +/src/fennel-ls/docs/generated/love2d.fnl diff --git a/Makefile b/Makefile index 3bd85a5..87a539e 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ repl: $(FENNEL) $(FENNELFLAGS) docs: - $(FENNEL) $(FENNELFLAGS) tools/get-docs.fnl + $(FENNEL) $(FENNELFLAGS) tools/get-docs.fnl $(GET_DOCS_FLAGS) rm-docs: rm -rf src/fennel-ls/docs/ diff --git a/docs/manual.md b/docs/manual.md index d3afa30..ecee8aa 100644 --- a/docs/manual.md +++ b/docs/manual.md @@ -70,7 +70,8 @@ The default `flsproject.fnl` settings are: {:fennel-path "./?.fnl;./?/init.fnl;src/?.fnl;src/?/init.fnl" :macro-path "./?.fnl;./?/init-macros.fnl;./?/init.fnl;src/?.fnl;src/?/init-macros.fnl;src/?/init.fnl" :lua-version "lua54" - :libraries {:tic-80 false} + :libraries {:love2d false + :tic-80 false} :extra-globals "" :lints {:unused-definition true :unknown-module-field true diff --git a/docs/packaging.md b/docs/packaging.md index b4dab70..e193068 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -21,6 +21,17 @@ $ make rm-docs $ make docs ``` +## Including LÖVE documentation +Due to license incompatibility between fennel-ls and the official LÖVE +documentation, this repository cannot include the LÖVE documentation by +default. It must be generated manually. + +This can be done by passing a flag to the `docs` target. + +```sh +$ make docs GET_DOCS_FLAGS=--generate-love2d +``` + ## Vendored Dependencies The vendored dependencies are very easy to solve. You delete the dependency files by running `make rm-deps`. diff --git a/src/fennel-ls/config.fnl b/src/fennel-ls/config.fnl index 3e1cbd4..42ad462 100644 --- a/src/fennel-ls/config.fnl +++ b/src/fennel-ls/config.fnl @@ -27,7 +27,7 @@ There are no global settings. They're all stored in the `server` object. :var-never-set (option true) :op-with-no-arguments (option true) :multival-in-middle-of-call (option true)} - :libraries {:tic-80 (option false)} + :libraries {:love2d (option false) :tic-80 (option false)} :extra-globals (option "")}) (fn make-configuration-from-template [default ?user ?parent] @@ -72,7 +72,7 @@ However, when not an option, fennel-ls will fall back to positionEncoding=\"utf- (λ load-config [server] "This is where we can put anything that needs to react to config changes" - + (make-configuration (when server.root-uri (-?> (files.read-file server (utils.path->uri (utils.path-join (utils.uri->path server.root-uri) "flsproject.fnl"))) diff --git a/src/fennel-ls/docs.fnl b/src/fennel-ls/docs.fnl index b6e7c8c..322220b 100644 --- a/src/fennel-ls/docs.fnl +++ b/src/fennel-ls/docs.fnl @@ -28,6 +28,10 @@ (local libraries {:tic-80 (require :fennel-ls.docs.generated.tic80)}) +;; can't just pcall require because we want to trigger require-as-include +(case (pcall #(require :fennel-ls.docs.generated.love2d)) + (true love2d) (set libraries.love2d love2d)) + (fn get-library [library] (when (not (. libraries library)) (error (.. "fennel-ls doesn't know about library " library "\n" @@ -44,13 +48,14 @@ (icollect [name (pairs (get-lua-version server.configuration.lua-version)) &into result] name))) +(fn get-library-global [server global-name] + (accumulate [g nil library-name enabled? (pairs server.configuration.libraries) + &until g] + (and enabled? (. (get-library library-name) global-name)))) + (fn get-global [server global-name] - (or - (and server.configuration.libraries.tic-80 - (. (get-library :tic-80) - global-name)) - (. (get-lua-version server.configuration.lua-version) - global-name))) + (or (get-library-global server global-name) + (. (get-lua-version server.configuration.lua-version) global-name))) (fn get-builtin [_server builtin-name] (or (. specials builtin-name) diff --git a/tools/get-docs.fnl b/tools/get-docs.fnl index e1cd8ad..0beb0b6 100644 --- a/tools/get-docs.fnl +++ b/tools/get-docs.fnl @@ -1,5 +1,7 @@ "Script to generate /src/fennel-ls/docs/lua54.fnl and friends automatically" + (local {: sh} (require :tools.util.sh)) +(local fennel (require :deps.fennel)) (fn curl-cached [url] (let [filename (.. "build/" (url:gsub "[/:]" "_")) @@ -10,24 +12,33 @@ (sh "curl" url [">"] filename) (io.open filename :r))))) +(fn write-doc-file! [out-filename doc-src doc-tbl] + (assert doc-tbl + "bad argument #3, `doc-tbl` to write-doc-file! (expected string, got nil)") + (with-open [file (io.open (.. :src/fennel-ls/docs/generated/ out-filename) :w)] + (file:write ";; auto-generated by `make docs` from fennel-ls. Contents come from " + doc-src "\n" doc-tbl "\n"))) + (fn derive-docs-from-url [url out-filename convert] - (let [result - (convert - (with-open [file (curl-cached url)] - (file:read "*a")))] - (with-open [file (io.open (.. "src/fennel-ls/docs/generated/" out-filename) :w)] - (file:write ";; auto-generated by `make docs` from fennel-ls. Contents come from " url "\n" - result "\n")))) + (write-doc-file! out-filename url + (convert (with-open [file (curl-cached url)] + (file:read :*a))))) (fn main [] (sh :mkdir :-p "build/") (sh :mkdir :-p "src/fennel-ls/docs/generated/") - (let [{:convert lua-manual} (require :tools.get-docs.lua-manual) + (let [generate-love2d-docs? (case arg ["--generate-love2d"] true _ false) + {:convert lua-manual} (require :tools.get-docs.lua-manual) {:convert tic-manual} (require :tools.get-docs.tic80)] (derive-docs-from-url "https://www.lua.org/manual/5.1/manual.html" "lua51.fnl" lua-manual) (derive-docs-from-url "https://www.lua.org/manual/5.2/manual.html" "lua52.fnl" lua-manual) (derive-docs-from-url "https://www.lua.org/manual/5.3/manual.html" "lua53.fnl" lua-manual) (derive-docs-from-url "https://www.lua.org/manual/5.4/manual.html" "lua54.fnl" lua-manual) - (derive-docs-from-url "https://tic80.com/learn" "tic80.fnl" tic-manual))) + (derive-docs-from-url "https://tic80.com/learn" :tic80.fnl tic-manual) + (when generate-love2d-docs? + (let [{:convert download-and-convert-love2d-manual!} (require :tools.get-docs.love2d)] + (write-doc-file! :love2d.fnl + "https://github.com/love2d-community/love-api/" + (download-and-convert-love2d-manual!)))))) (main) diff --git a/tools/get-docs/love2d.fnl b/tools/get-docs/love2d.fnl new file mode 100644 index 0000000..afe363e --- /dev/null +++ b/tools/get-docs/love2d.fnl @@ -0,0 +1,104 @@ +(local fennel (require :deps.fennel)) +(local {:clone git-clone} (require :tools.util.git)) + +(local love-api-build-directory :build/love-api) +(local require-love-api + (partial require (.. love-api-build-directory :/love_api))) + +; +; UTILS +; ----- +(fn build-lsp-value [name ?args ?docstring ?fields] + "Takes ... and returns a table to be used with the LSP." + (let [lsp-value {:binding name} + ?metadata (or ?args ?docstring)] + (when ?metadata (set lsp-value.metadata {})) + (when ?args (set lsp-value.metadata.fnl/arglist ?args)) + (when ?docstring (set lsp-value.metadata.fnl/docstring ?docstring)) + (when ?fields (set lsp-value.fields ?fields)) + lsp-value)) + +(fn clone-love-api! [] + (when (not (io.open :build/love-api)) + (git-clone love-api-build-directory + "https://github.com/love2d-community/love-api"))) + +(fn merge [...] + (let [arg-count (select "#" ...) + args [...]] + (if (= arg-count 0) + {} + (faccumulate [result {} i 1 arg-count] + (collect [k v (pairs (. args i)) &into result] + (values k v)))))) + +; +; PARSERS +; ------- +(fn get-fn-argument-names [fn-arguments] + "Given an array of arguments, return all names as an array." + (icollect [_i {:description _ : name :type _} (ipairs fn-arguments)] + name)) + +(fn format-description-of-fn-return-values [fn-returns] + (accumulate [x "\n\nReturns -" _i {: description : name :type return-type} (ipairs fn-returns)] + (.. x "\n" " * " name " (`" return-type "`) - " description))) + +(fn parse-first-fn-variant [[variant]] + "Given an array of fuction variants, format and return the first variant + for the LSP." + (collect [k v (pairs variant)] + (case k + :returns (values :returns (format-description-of-fn-return-values v)) + :arguments (values :args (get-fn-argument-names v))))) + +(fn get-all-love-api-functions [love-api] + [(table.unpack love-api.functions) (table.unpack love-api.callbacks)]) + +(fn love-functions->lsp-table [docs-tbl namespace] + (collect [_i value (ipairs docs-tbl)] + (let [{: name : description} value + binding (.. namespace name) + ?variants (?. value :variants) + ; LÖVE functions have several variants, e.g. different arities or + ; types; however, it's uncertain how to best display all of that + ; information, so the first is selected here as a reasonable default. + first-variant (if ?variants + (parse-first-fn-variant ?variants) + nil) + ?args (?. first-variant :args) + ?returns (or (?. first-variant :returns) "") + docstring (.. description ?returns)] + (values name (build-lsp-value binding ?args docstring))))) + +(fn module-list->lsp-table [modules ?namespace] + (collect [_i module (ipairs modules)] + (let [{: name} module ; Other keys - :enum, :functions, :types + namespace (if ?namespace (.. ?namespace ".") "") + binding (.. namespace name) + ?docstring (?. module :description) + ?functions (?. module :functions) + ?modules (?. module :modules) + function-keys (if ?functions + (love-functions->lsp-table ?functions (.. binding ".")) + {}) + module-keys (if ?modules (module-list->lsp-table ?modules binding) {}) + fields (merge function-keys module-keys)] + (values name (build-lsp-value binding nil ?docstring fields))))) + +(fn love-api->lsp-table [love-api] + (let [root-module {:description (.. "LÖVE is a framework for making 2D " + "games in the Lua programming language.") + :functions (get-all-love-api-functions love-api) + :modules love-api.modules + :name :love}] + (module-list->lsp-table [root-module]))) + +(fn convert [] + "Download documentation for the LÖVE framework via the love-api repo and + convert it to a Lua table usable for fennel-ls." + (clone-love-api!) + (let [love-api (require-love-api)] + (fennel.view (love-api->lsp-table love-api)))) + +{: convert} diff --git a/tools/util/git.fnl b/tools/util/git.fnl new file mode 100644 index 0000000..ebc9727 --- /dev/null +++ b/tools/util/git.fnl @@ -0,0 +1,12 @@ +(local {: sh} (require :tools.util.sh)) + +(fn clone [location url ?tag] + "Clones a git repository, given a location, url, and optional tag." + (assert location "Expected file location to clone git repository into.") + (assert url "Expected git repository url to clone.") + (if ?tag + (sh :git :clone :-c :advice.detachedHead=false :--depth=1 :--branch ?tag + url location) + (sh :git :clone :-c :advice.detachedHead=false :--depth=1 url location))) + +{: clone}