Adds support for hovering over symbols

This commit is contained in:
XeroOl 2022-08-14 22:12:55 -05:00
parent c2cd1e50fe
commit 22b4235073
No known key found for this signature in database
GPG Key ID: 9DD4B4B4DAED0322
11 changed files with 228 additions and 70 deletions

View File

@ -13,7 +13,7 @@ Features / To Do List / Things I would enjoy patches for:
- [ ] Go-to-definition
- [ ] directly on require statements
- [X] for definitions in the same file
- [ ] for definitions in other files
- [X] for definitions in other files
- [X] follows multisyms through table constructor
- [ ] follows multisyms through mutations (difficult)
- [ ] for methods/metamethods (difficult, in the general case may require type annotations or some insane global type inference logic)
@ -23,7 +23,7 @@ Features / To Do List / Things I would enjoy patches for:
- [ ] including in macro files
- [ ] Reports linting issues
- [ ] Completion Suggestions
- [ ] Hover over a symbol for documentation
- [X] Hover over a symbol for documentation
- [ ] Signature help
- [ ] Go-to-references on definition sites
- [ ] integration with fnlfmt

View File

@ -59,8 +59,9 @@
(let [definition
{: binding
: ?definition
:?keys (fcollect [i 1 (length keys)]
(. keys i))}]
:?keys (if (not= 0 (length keys))
(fcollect [i 1 (length keys)]
(. keys i)))}]
(tset (. definitions-by-scope scope) (tostring binding) definition)
(tset definitions binding definition))
(= :table (type binding))
@ -77,7 +78,7 @@
(and (fennel.sym? name)
(not (multisym? name)) ;; not dealing with multisym for now
(fennel.sequence? args)))
(tset (. definitions-by-scope scope) ;; !!! parent or child?
(tset (. definitions-by-scope scope) ;; !!! TODO somehow insert into child scope
(tostring name)
{:binding name
:?definition ast})))

View File

@ -0,0 +1,50 @@
(local {: sym
: sym?
: view
: list} (require :fennel))
(local {: type=} (require :fennel-ls.utils))
(local -fn- (sym :fn))
(local -varg- (sym :...))
(λ code-block [str]
(.. "```fnl\n" str "\n```"))
(local width 80)
(fn fn-format [name args docstring]
(.. "(fn"
(if name (.. " " (tostring name)) "")
(.. " " (view args {:one-line? true :prefer-colon? true}))
" ...)"
(if docstring (.. "\n" docstring) "")))
(λ hover-format [result]
(code-block
(match result.?definition
;; name + docstring
(where [-fn- name args docstring body]
(and (sym? name)
(type= args :table)
(type= docstring :string)))
(fn-format name args docstring)
;; docstring
(where [-fn- args docstring body]
(and (type= args :table)
(type= docstring :string)))
(fn-format nil args docstring)
;; name
(where [-fn- name args]
(and (sym? name)
(type= args :table)))
(fn-format name args nil)
;; none
(where [-fn- args]
(and (type= args :table)))
(fn-format nil args nil)
?anything-else
(if result.?keys
(view result.?keys)
(view ?anything-else {:prefer-colon? true})))))
{: hover-format}

View File

@ -8,6 +8,8 @@ Every time the client sends a message, it gets handled by a function in the corr
(local message (require :fennel-ls.message))
(local state (require :fennel-ls.state))
(local language (require :fennel-ls.language))
(local formatter (require :fennel-ls.formatter))
(local requests [])
(local notifications [])
@ -16,7 +18,7 @@ Every time the client sends a message, it gets handled by a function in the corr
{:textDocumentSync 1 ;; FIXME: upgrade to 2
;; :notebookDocumentSync nil
;; :completionProvider nil
;; :hoverProvider nil
:hoverProvider {:workDoneProgress false}
;; :signatureHelpProvider nil
;; :declarationProvider nil
:definitionProvider {:workDoneProgress false}})
@ -63,9 +65,24 @@ Every time the client sends a message, it gets handled by a function in the corr
(local byte (pos->byte file.text position.line position.character))
(match (language.find-symbol file.ast byte)
symbol
(match (language.search-main self file symbol [])
(definition result-file) ;; curse you, magical match rules
(message.range-and-uri definition result-file))))
(match (language.search-main self file symbol)
(result result-file) ;; curse you, magical match rules
(message.range-and-uri
(or result.binding result.?definition)
result-file))))
(λ requests.textDocument/hover [self send {: position :textDocument {: uri}}]
(local file (state.get-by-uri self uri))
(local byte (pos->byte file.text position.line position.character))
(match (language.find-symbol file.ast byte)
symbol
(match (language.search-main self file symbol)
result
{:contents
{:kind
"markdown"
:value
(formatter.hover-format result)}})))
(λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}]
(local file (state.get-by-uri self uri))

View File

@ -8,50 +8,53 @@
(local sym? fennel.sym?)
(local list? fennel.list?)
(var
(search-assignment
search-symbol
search)
nil)
(local -require- (fennel.sym :require))
(local -dot- (fennel.sym :.))
(set search-assignment
(λ search-assignment [self file {: binding : ?definition : ?keys} stack]
(if (= 0 (length stack))
(values binding file) ;; BASE CASE!!
(do
(if ?keys
(fcollect [i (length ?keys) 1 -1 &into stack]
(. ?keys i)))
(search self file ?definition stack)))))
(var search nil) ;; all of the search functions are mutually recursive
(set search-symbol
(λ search-symbol [self file symbol stack]
(let [split (utils.multi-sym-split symbol)]
(λ search-assignment [self file {: binding : ?definition : ?keys &as assignment} stack]
(if (= 0 (length stack))
(values assignment file) ;; BASE CASE!!
(do
(if ?keys
(fcollect [i (length ?keys) 1 -1 &into stack]
(. ?keys i)))
(search self file ?definition stack))))
(λ search-symbol [self file symbol stack]
(let [split (utils.multi-sym-split symbol)]
(fcollect [i (length split) 2 -1 &into stack]
(. split i))) ;; TODO test coverage for this line
(match (. file.references symbol)
to (search-assignment self file to stack)))
(λ search-table [self file tbl stack]
(if (. tbl (. stack (length stack)))
(search self file (. tbl (table.remove stack)) stack)
(= 0 (length stack))
(values {:?definition tbl} file) ;; BASE CASE !!
nil)) ;; BASE CASE Give up
(λ search-list [self file call stack]
(match call
[-require- mod]
(let [newfile (state.get-by-module self mod)
newitem (. newfile.ast (length newfile.ast))]
(search self newfile newitem stack))
[-dot- & split]
(do
(fcollect [i (length split) 2 -1 &into stack]
(. split i))) ;; TODO test coverage for this line
(match (. file.references symbol)
to (search-assignment self file to stack))))
(. split i))
(search self file (. split 1) stack))))
(set search
(λ search [self file item stack]
(if
(fennelutils.table? item)
(if (. item (. stack (length stack)))
(search self file (. item (table.remove stack)) stack)
(= 0 (length stack))
(values item file) ;; BASE CASE !!
nil) ;; BASE CASE Give up
(sym? item)
(search-symbol self file item stack)
;; TODO
;; functioncall (continue searching in body with parameters bound)
(match item
[-require- mod]
(let [newfile (state.get-by-module self mod)
newitem (. newfile.ast (length newfile.ast))]
(search self newfile newitem stack))
_ (error (.. "I don't know what to do with " (fennel.view item)))))))
(if (fennelutils.table? item) (search-table self file item stack)
(sym? item) (search-symbol self file item stack)
(list? item) (search-list self file item stack)
(= 0 (length stack)) {:?definition item} ;; BASE CASE !!
(error (.. "I don't know what to do with " (fennel.view item))))))
(λ search-main [self file symbol]
;; TODO partial byting, go to different defitition sites depending on which section of the symbol the trigger happens on
@ -67,7 +70,12 @@
(ref _)
(search-assignment self file ref stack)
(_ def)
(search self file def.?definition stack)))
(do
(if def.?keys
(fcollect [i (length def.?keys) 1 -1 &into stack]
(. def.?keys i)))
(search self file def.?definition stack))))
;; (search self file def.?definition stack))))
(λ past? [?ast byte]
;; check if a byte is past an ast object
@ -96,9 +104,8 @@
(+ 1 (get-ast-info ?ast :byteend))))))
(λ find-symbol [ast byte ?recursively-called]
(if (not= :table (type ast))
nil
(does-not-contain? ast byte)
(if (or (not= :table (type ast))
(does-not-contain? ast byte))
nil
(and (sym? ast) (contains? ast byte))
ast
@ -121,5 +128,4 @@
(find-symbol v byte true)))))
{: find-symbol
: search-symbol
: search-main}

View File

@ -84,10 +84,14 @@ These functions are all pure functions, which makes me happy."
(icollect [word (: (.. sym ".") :gmatch "(.-)[%.:]")]
word))
(λ type= [val typ]
(= (type val) typ))
{: uri->path
: path->uri
: pos->byte
: byte->pos
: apply-changes
: multi-sym-split
: get-ast-info}
: get-ast-info
: type=}

View File

@ -26,47 +26,49 @@
(.. "expected position: " start-line " " start-col " " end-line " " end-col))))
(it "can go to a fn"
(check "example.fnl" 9 3 "example.fnl" 4 4 4 7))
(check :goto-definition.fnl 9 3 :goto-definition.fnl 4 4 4 7))
(it "can go to a local"
(check "example.fnl" 7 17 "example.fnl" 6 9 6 10))
(check :goto-definition.fnl 7 17 :goto-definition.fnl 6 9 6 10))
(it "can go to a function argument"
(check "example.fnl" 5 9 "example.fnl" 4 9 4 10))
(check :goto-definition.fnl 5 9 :goto-definition.fnl 4 9 4 10))
(it "can handle variables shadowed with let"
(check "example.fnl" 14 10 "example.fnl" 13 6 13 9))
(check :goto-definition.fnl 14 10 :goto-definition.fnl 13 6 13 9))
(it "can sort out the unification rule with match (variable unified)"
(check "example.fnl" 19 12 "example.fnl" 17 8 17 9))
(check :goto-definition.fnl 19 12 :goto-definition.fnl 17 8 17 9))
(it "can sort out the unification rule with match (variable introduced)"
(check "example.fnl" 20 13 "example.fnl" 20 9 20 10))
(check :goto-definition.fnl 20 13 :goto-definition.fnl 20 9 20 10))
(it "can go to a destructured local"
(check "example.fnl" 21 9 "example.fnl" 16 13 16 16))
(check :goto-definition.fnl 21 9 :goto-definition.fnl 16 13 16 16))
(it "can go to a function inside a table"
(check "example.fnl" 28 6 "example.fnl" 4 4 4 7))
(check :goto-definition.fnl 28 6 :goto-definition.fnl 4 4 4 7))
;; (it "can go to a field inside of a table")
(it "can go to a field inside of a table literal"
(check :goto-definition.fnl 35 19 :goto-definition.fnl 34 20 34 35))
(it "can go to a function in another file when accessed by multisym"
(check "example.fnl" 7 7 "foo.fnl" 2 4 2 13))
(check :goto-definition.fnl 7 7 :foo.fnl 2 4 2 13))
(it "goes further if you go to definition on a binding"
(check "example.fnl" 31 12 "example.fnl" 23 4 23 5))
(check :goto-definition.fnl 31 12 :goto-definition.fnl 23 4 23 5))
;; (it "can go to a destructured function argument")
;; it can go up and down destructuring
(it "can trace a variable that was introduced with destructuring assignment"
(check "example.fnl" 38 15 "example.fnl" 33 7 33 13)))
(it "can go up and down destructuring"
(check :goto-definition.fnl 38 15 :goto-definition.fnl 33 7 33 13))
(it "can go up and down field accesses"
(check :goto-definition.fnl 45 15 :goto-definition.fnl 40 7 40 13)))
;; (it "works directly on a require/include (require XXX))"
;; (check "example.fnl" 1 5 "bar.fnl" 0 0 0 0))
;; (check :goto-definition.fnl 1 5 :bar.fnl 0 0 0 0)))
;; (it "can go to a reference that occurs in a macro")
;; (it "doesn't have ghost definitions from the same byte ranges as the macro files it's using")

44
test/hover-test.fnl Normal file
View File

@ -0,0 +1,44 @@
(import-macros {: is-matching : describe : it : before-each} :test.macros)
(local is (require :luassert))
(local fennel (require :fennel))
(local {: ROOT-URI
: setup-server} (require :test.util))
(local dispatch (require :fennel-ls.dispatch))
(local message (require :fennel-ls.message))
(describe "hover"
(fn check [request-file line char response-string]
(local state (doto [] setup-server))
(let [message (dispatch.handle* state
(message.create-request 2 "textDocument/hover"
{:position {:character char :line line}
:textDocument {:uri (.. ROOT-URI "/" request-file)}}))]
(is-matching
message
[{:jsonrpc "2.0" :id 2
:result
{:contents
{:kind "markdown"
:value response-string}}}]
(.. "expected response: " (fennel.view response-string)))))
(it "hovers over a function"
(check "hover.fnl" 6 6 "```fnl\n(fn my-function [arg1 arg2 arg3] ...)\n```"))
(it "hovers over a literal number"
(check "hover.fnl" 6 16 "```fnl\n300\n```"))
(it "hovers over a literal string"
(check "hover.fnl" 6 19 "```fnl\n\"some text\"\n```"))
(it "hovers over a field number"
(check "hover.fnl" 9 20 "```fnl\n10\n```"))
(it "hovers over a field string"
(check "hover.fnl" 9 30 "```fnl\n:colon-string\n```"))
(it "hovers over a literal nil"
(check "hover.fnl" 12 9 "```fnl\nnil\n```")))

View File

@ -4,4 +4,5 @@
(require :test.string-processing-test)
(require :test.lsp-test)
(require :test.goto-definition-test)
(require :test.hover-test)
(require :test.misc-test)

View File

@ -42,5 +42,5 @@
(local deep {:a {:b {:field findme}}})
(local shallow deep.a.b)
(local mixed [{:key [5 {:foo shallow}]}])
(local funny (. mixed 1 :key 2 foo))
(local funny (. mixed 1 :key 2 :foo))
(print funny.field)

View File

@ -0,0 +1,33 @@
(fn my-function [arg1 arg2 arg3]
(let [result nil]
result))
(local foo 300)
(let [bar "some text"]
(my-function foo bar 3))
(local foo {:field1 10 :field2 :colon-string})
(my-function foo.field1 foo.field2)
(local empty nil)
(print empty)
(fn sd [] "short docstring"
nil)
(fn ld [arg1]
"long docstring
This function has a long docstring, and returns nil.
The docstring has newlines and markdown and stuff in it.
```fnl
(ld 100 100) ;; ==> nil
```
@arg arg1 is ignored
@arg arg2 is ignored.
@returns nil"
(let [result nil]
result))
(ld (sd))