From 3eeba961883db72aa589019a7c0fd4fd40e770ae Mon Sep 17 00:00:00 2001 From: XeroOl Date: Wed, 24 Aug 2022 23:12:28 -0500 Subject: [PATCH] Refactoring / documentation comments / small fixes --- src/fennel-ls/compiler.fnl | 64 +++++++++++++++----------- src/fennel-ls/formatter.fnl | 6 +++ src/fennel-ls/language.fnl | 4 ++ src/fennel-ls/state.fnl | 7 +++ src/fennel-ls/utils.fnl | 20 ++++---- test/completion-test.fnl | 36 +++++++++++++++ test/diagnostic-test.fnl | 86 ++++++++++++++++++++++++----------- test/goto-definition-test.fnl | 10 +++- test/init.fnl | 9 ++-- 9 files changed, 175 insertions(+), 67 deletions(-) create mode 100644 test/completion-test.fnl diff --git a/src/fennel-ls/compiler.fnl b/src/fennel-ls/compiler.fnl index bf9074f..bb2ea73 100644 --- a/src/fennel-ls/compiler.fnl +++ b/src/fennel-ls/compiler.fnl @@ -1,3 +1,8 @@ +"Compiler +This file is responsible for the low level tasks of analysis. Its main job +is to recieve a file object and run all of the basic analysis that will be used +later by fennel-ls.language to answer requests from the client." + (local {: sym? : list? : sequence? : sym : view &as fennel} (require :fennel)) (local message (require :fennel-ls.message)) @@ -32,6 +37,7 @@ (λ compile [file] "Compile the file, and record all the useful information from the compiler into the file object" + ;; The useful information being recorded: (let [definitions-by-scope (doto {} (setmetatable has-tables-mt)) definitions {} diagnostics {} @@ -115,8 +121,9 @@ [-require- modname] (tset require-calls ast true))) - (λ on-compiler-error [_ msg ast call-me-to-reset-the-compiler] - (let [range (message.ast->range ast file)] + (λ on-compile-error [_ msg ast call-me-to-reset-the-compiler] + (let [range (or (message.ast->range ast file) + (message.pos->range 0 0 0 0))] (table.insert diagnostics {:range range :message msg @@ -126,6 +133,17 @@ (call-me-to-reset-the-compiler) (error "__NOT_AN_ERROR")) + (λ on-parse-error [msg file line byte] + ;; assume byte and char count is the same, ie no UTF-8 + (let [range (message.pos->range line byte line byte)] + (table.insert diagnostics + {:range range + :message msg + :severity message.severity.ERROR + :code 201 + :codeDescription "compiler error"})) + (error "__NOT_AN_ERROR")) + ;; TODO clean up this code. It's awful now that there is error handling (let [plugin @@ -134,31 +152,23 @@ :symbol-to-expression reference :call call :destructure define - :assert-compile on-compiler-error}] - - ;; ATTEMPT TO PARSE AST - (match (pcall - #(icollect [ok ast (fennel.parser file.text file.uri {:plugins [plugin]})] - ast)) - ;; ON SUCCESS - (true ast) - (let [scope (fennel.scope)] - (each [_i form (ipairs ast)] - ;; COMPILE - (match (pcall fennel.compile form {:filename file.uri - :plugins [plugin] - :allowedGlobals (icollect [k v (pairs _G)] k) - :requireAsInclude false - : scope}) - (where (nil err) (not= err "__NOT_AN_ERROR")) - (error err))) - (set file.ast ast)) - ;; ON FAILURE - (false err) - ;; RECORD THE FAILURE - (table.insert diagnostics - {:range (message.pos->range 0 0 0 0) - :message err})) + :assert-compile on-compile-error + :parse-error on-parse-error} + scope (fennel.scope) + opts {:filename file.uri + :plugins [plugin] + :allowedGlobals (icollect [k v (pairs _G)] k) + :requireAsInclude false + : scope} + parser (partial pcall (fennel.parser file.text file.uri opts)) + ast (icollect [ok ok2 ast parser &until (not (and ok ok2))] ast) + _compile-output (icollect [_i form (ipairs ast)] + (match (pcall fennel.compile form opts) + (where (nil err) (not= err "__NOT_AN_ERROR")) + (table.insert diagnostics + {:range (message.pos->range 0 0 0 0) + :message err})))] + (set file.ast ast) ;; write things back to the file object diff --git a/src/fennel-ls/formatter.fnl b/src/fennel-ls/formatter.fnl index 0c04c9f..c4d1ee4 100644 --- a/src/fennel-ls/formatter.fnl +++ b/src/fennel-ls/formatter.fnl @@ -1,3 +1,8 @@ +"Formatter +This module is for formatting code that needs to be shown to the client +in tooltips and other notification messages. It is NOT for formatting +user code." + (local {: sym : sym? : view @@ -20,6 +25,7 @@ (if docstring (.. "\n" docstring) ""))) (λ hover-format [result] + "Format code that will appear when the user hovers over a symbol" (code-block (match result.?definition ;; name + docstring diff --git a/src/fennel-ls/language.fnl b/src/fennel-ls/language.fnl index 24471e3..279ab61 100644 --- a/src/fennel-ls/language.fnl +++ b/src/fennel-ls/language.fnl @@ -1,3 +1,7 @@ +"Language +The high level analysis system that does deep searches following +the data provided by compiler.fnl." + (local {: sym? : list? : sequence? : sym : view} (require :fennel)) (local utils (require :fennel-ls.utils)) (local state (require :fennel-ls.state)) diff --git a/src/fennel-ls/state.fnl b/src/fennel-ls/state.fnl index 4c0eb80..67e1065 100644 --- a/src/fennel-ls/state.fnl +++ b/src/fennel-ls/state.fnl @@ -1,3 +1,10 @@ +"State +This module keeps track of the state of the language server. +There are helpers to get file objects, and in the future, there +will be functions for managing user options. There is no global +state in this project: all state will be stored in the \"self\" +object." + (local utils (require :fennel-ls.utils)) (local searcher (require :fennel-ls.searcher)) (local {: compile} (require :fennel-ls.compiler)) diff --git a/src/fennel-ls/utils.fnl b/src/fennel-ls/utils.fnl index 4f1e180..3778ec0 100644 --- a/src/fennel-ls/utils.fnl +++ b/src/fennel-ls/utils.fnl @@ -75,14 +75,18 @@ These functions are all pure functions, which makes me happy." (or (?. (getmetatable ?ast) info) (. ?ast info))) -(fn multi-sym-split [sym ?offset] - (local sym (tostring sym)) - (local offset (or ?offset (length sym))) - (local next-separator (or (sym:find ".[%.:]" offset) - (length sym))) - (local sym (sym:sub 1 next-separator)) - (icollect [word (: (.. sym ".") :gmatch "(.-)[%.:]")] - word)) +(fn multi-sym-split [symbol ?offset] + (local symbol (tostring symbol)) + (if (or (= symbol ".") + (= symbol "..") + (= symbol "...")) + [symbol] + (let [offset (or ?offset (length symbol)) + next-separator (or (symbol:find ".[.:]" offset) + (length symbol)) + symbol (symbol:sub 1 next-separator)] + (icollect [word (: (.. symbol ".") :gmatch "(.-)[.:]")] + word)))) (λ type= [val typ] (= (type val) typ)) diff --git a/test/completion-test.fnl b/test/completion-test.fnl new file mode 100644 index 0000000..e00e776 --- /dev/null +++ b/test/completion-test.fnl @@ -0,0 +1,36 @@ +(import-macros {: is-matching : describe : it : before-each} :test) +(local is (require :luassert)) + +(local {: view} (require :fennel)) +(local {: ROOT-URI + : setup-server} (require :test.util)) + +(local dispatch (require :fennel-ls.dispatch)) +(local message (require :fennel-ls.message)) + +(local FILENAME (.. ROOT-URI "imaginary-file.fnl")) + +(fn open-file [state text] + (dispatch.handle* state + (message.create-notification "textDocument/didOpen" + {:textDocument + {:uri FILENAME + :languageId "fennel" + :version 1 + : text}}))) + +(describe "completions") + ;; (it "suggests globals" + ;; (local state (doto [] setup-server)) + ;; ;; empty file + ;; (open-file state "") + ;; (let [response (dispatch.handle* state + ;; (message.create-request 2 "textDocument/completion" + ;; {:position {:line 0 :character 0} + ;; :textDocument {:uri FILENAME}}))] + ;; (is-matching response nil "oops")))) + + ;; (it "suggests locals in scope") + ;; (it "does not suggest locals out of scope") + ;; (it "suggests fields of tables") + ;; (it "knows what fields are meant to be inside of globals") diff --git a/test/diagnostic-test.fnl b/test/diagnostic-test.fnl index 4af9351..73ee148 100644 --- a/test/diagnostic-test.fnl +++ b/test/diagnostic-test.fnl @@ -8,35 +8,69 @@ (local dispatch (require :fennel-ls.dispatch)) (local message (require :fennel-ls.message)) +(macro find [t body ?sentinel] + (assert-compile (not ?sentinel) "you can only have one thing here, put a `(do)`") + (assert-compile (sequence? t) "[] square brackets please") + (local result (gensym :result)) + (local nil* (sym :nil)) + (table.insert t 1 result) + (table.insert t 2 nil*) + (table.insert t `&until) + (table.insert t result) + `(accumulate ,t ,body)) + +(fn open-file [state text] + (dispatch.handle* state + (message.create-notification "textDocument/didOpen" + {:textDocument + {:uri (.. ROOT-URI "imaginary-file.fnl") + :languageId "fennel" + :version 1 + : text}}))) + (describe "diagnostic messages" (it "handles compile errors" (local state (doto [] setup-server)) - (let - [responses - (dispatch.handle* state - (message.create-notification "textDocument/didOpen" - {:textDocument - {:uri (.. ROOT-URI "imaginary-file.fnl") - :languageId "fennel" - :version 1 - :text "(do do)"}}))] - (is-matching - responses - [{:params {:diagnostics [diagnostic]}}] - ""))) + (let [responses (open-file state "(do do)") + diagnostic + (match responses + [{:params {: diagnostics}}] + (find [i v (ipairs diagnostics)] + (match v + {:message "tried to reference a special form at runtime" + :range {:start {:character 4 :line 0} + :end {:character 6 :line 0}}} + v)))] + (is diagnostic "expected a diagnostic"))) (it "handles parse errors" (local state (doto [] setup-server)) - (let - [responses - (dispatch.handle* state - (message.create-notification "textDocument/didOpen" - {:textDocument - {:uri (.. ROOT-URI "imaginary-file.fnl") - :languageId "fennel" - :version 1 - :text "(do (print :hello(]"}}))] - (is-matching - responses - [{:params {:diagnostics [diagnostic]}}] - "")))) + (let [responses (open-file state "(do (print :hello(]") + diagnostic + (match responses + [{:params {: diagnostics}}] + (find [i v (ipairs diagnostics)] + (match v + {:message "expected whitespace before opening delimiter (" + :range {:start {:character 17 :line 1} + :end {:character 17 :line 1}}} + v)))] + (is diagnostic "expected a diagnostic"))) + + (it "handles (match)" + (local state (doto [] setup-server)) + (let [responses (open-file state "(match)")] + (is-matching responses + [{:params + {:diagnostics + [{:range {:start {:character a :line b} + :end {:character c :line d}}}]}}] + "diagnostics should always have a range")))) + +;; TODO lints: +;; unnecessary (do) in body position +;; Unused variables / fields (maybe difficult) +;; discarding results to various calls +;; unnecessary `do`/`values` with only one inner form +;; mark when unification is happening on a `match` pattern (may be difficult) +;; think of more lints diff --git a/test/goto-definition-test.fnl b/test/goto-definition-test.fnl index 9ae9602..3aee2d1 100644 --- a/test/goto-definition-test.fnl +++ b/test/goto-definition-test.fnl @@ -70,12 +70,18 @@ ;; TODO ;; (it "can go to a function in another file imported via destructuring assignment") ;; WORKS, just needs a test case - ;; (it "doesn't have ghost definitions from the same byte ranges as the macro files it's using") ;; Unconfirmed, I saw some weird behavior a while ago ;; (it "can go through more than one extra file") ;; (it "will give up instead of freezing on recursive requires") ;; (it "finds the definition of in-file macros") ;; (it "can follow import-macros (destructuring)") ;; (it "can follow import-macros (namespaced)") ;; (it "can go to the definition even in a lua file") - ;; (it "can go to a function's arguments when they're available") + ;; (it "finds (fn a.b [] ...) declarations") + ;; (it "finds (set a.b) definitions") + ;; (it "finds (tset a :b) definitions") + ;; (it "finds (setmetatable a {__index {:b def}) definitions") + ;; (it "finds definitions from inside a function (fn foo [] (local x 10) {: x}) (let [result (foo)] (print result.x)) finds result.x") + ;; (it "finds basic setmetatable definitions with an __index function") + ;; (it "can return to callsite and go through a function's arguments when they're available") + ;; (it "can go to a function's reference OR read type inference comments when callsite isn't available (PICK ONE)") ;; (it "can work with a custom fennelpath") ;; Wait until an options system is done diff --git a/test/init.fnl b/test/init.fnl index ebb3b25..171e964 100644 --- a/test/init.fnl +++ b/test/init.fnl @@ -1,9 +1,10 @@ ((require :busted.runner)) -(require :test.json-rpc-test) -(require :test.string-processing-test) -(require :test.lsp-test) +(require :test.completion-test) +(require :test.diagnostic-test) (require :test.goto-definition-test) (require :test.hover-test) +(require :test.json-rpc-test) +(require :test.lsp-test) (require :test.misc-test) -(require :test.diagnostic-test) +(require :test.string-processing-test)