Adds support for hovering over symbols
This commit is contained in:
parent
c2cd1e50fe
commit
22b4235073
@ -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
|
||||
|
||||
@ -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})))
|
||||
|
||||
50
src/fennel-ls/formatter.fnl
Normal file
50
src/fennel-ls/formatter.fnl
Normal 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}
|
||||
@ -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))
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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=}
|
||||
|
||||
@ -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
44
test/hover-test.fnl
Normal 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```")))
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
33
test/test-project/hover.fnl
Normal file
33
test/test-project/hover.fnl
Normal 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))
|
||||
Loading…
Reference in New Issue
Block a user