added all of the code
This commit is contained in:
parent
349280ad9e
commit
01f76e4c4f
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
||||
fennel-ls
|
||||
^fennel-ls$
|
||||
|
||||
72
src/fennel-ls/dispatch.fnl
Normal file
72
src/fennel-ls/dispatch.fnl
Normal file
@ -0,0 +1,72 @@
|
||||
"Dispatch
|
||||
This module is responsible for deciding which code to call in response
|
||||
to a given LSP request from the client.
|
||||
|
||||
In general, this involves:
|
||||
* parsing the message
|
||||
* determining the type of the message
|
||||
* calling the appropriate handler"
|
||||
|
||||
(local handlers (require :fennel-ls.the-actual-code))
|
||||
(local message (require :fennel-ls.message))
|
||||
|
||||
(λ handle-request [self send id method ?params]
|
||||
"Call the appropriate request handler.
|
||||
The return value of the request is sent back to the server."
|
||||
(match (. handlers.requests method)
|
||||
callback
|
||||
(match (callback self send ?params)
|
||||
(nil err) (send (message.create-error :InternalError err id))
|
||||
?response (send (message.create-response id ?response)))
|
||||
nil
|
||||
(send
|
||||
(message.create-error
|
||||
:MethodNotFound
|
||||
(.. "\"" method "\" is not in the request-handlers table")
|
||||
id))))
|
||||
|
||||
(λ handle-response [self send id result]
|
||||
"I don't care about responses yet"
|
||||
nil)
|
||||
|
||||
(λ handle-bad-response [self send id err]
|
||||
"Handle a message indicating an error. Right now, it just crashes the server."
|
||||
(error (.. "oopsie: " err.code)))
|
||||
|
||||
(λ handle-notification [self send method ?params]
|
||||
"Call the appropriate notification handler."
|
||||
(match (. handlers.notifications method)
|
||||
callback (callback self send ?params)))
|
||||
;; Silent error for unknown notifications
|
||||
|
||||
(λ handle [self send msg]
|
||||
"Figures out what to do with a message.
|
||||
This can involve updating the state of the server, and/or sending messages to the
|
||||
server.
|
||||
|
||||
Takes:
|
||||
* `self`, which is the state of the server,
|
||||
* `send`, which is a callback for sending responses, and
|
||||
* `msg`, which is the message to receive."
|
||||
(match (values msg (type msg))
|
||||
{:jsonrpc "2.0" : id : method :params ?params}
|
||||
(handle-request self send id method ?params)
|
||||
{:jsonrpc "2.0" : method :params ?params}
|
||||
(handle-notification self send method ?params)
|
||||
{:jsonrpc "2.0" : id : result}
|
||||
(handle-response self send id result)
|
||||
{:jsonrpc "2.0" : id :error err}
|
||||
(handle-bad-response self send id err)
|
||||
(str :string)
|
||||
(send (message.create-error :ParseError str))
|
||||
_
|
||||
(send (message.create-error :BadMessage nil msg.id))))
|
||||
|
||||
(λ handle* [self msg]
|
||||
"handles a message, and returns all the responses in a table"
|
||||
(let [out []]
|
||||
(handle self (partial table.insert out) msg)
|
||||
out))
|
||||
|
||||
{: handle
|
||||
: handle*}
|
||||
64
src/fennel-ls/json-rpc.fnl
Normal file
64
src/fennel-ls/json-rpc.fnl
Normal file
@ -0,0 +1,64 @@
|
||||
"JSON-RPC
|
||||
This module implements the parsing and formatting code needed to read/write messages over the language server protocol.
|
||||
There are only two functions exposed here:
|
||||
|
||||
* `read` receives and parses a message from the client.
|
||||
* `write` serializes and sends a message to the client.
|
||||
|
||||
It's probably not compliant yet, because serialization of [] and {} is the same,
|
||||
and there are also some places where a field has to be present, but filled with null.
|
||||
|
||||
Luckily, I'm testing with Neovim, so I can pretend these problems don't exist for now."
|
||||
|
||||
;; TODO find json library that doesn't conflate missing fields with null
|
||||
(local {: encode : decode} (require :json.json))
|
||||
(local {: split} (require :pl.stringx))
|
||||
|
||||
(λ read-header [in ?header]
|
||||
"Reads the header of a JSON-RPC message"
|
||||
(let [header (or ?header {})]
|
||||
(match (in:read)
|
||||
"\r" header ;; hit an empty line, I'm done reading
|
||||
nil nil ;; hit end of stream, return nil
|
||||
;; reading an actual line
|
||||
header-line
|
||||
(let [[k v] (split header-line ": " 2)]
|
||||
(tset header k (string.sub v 1 -2))
|
||||
(read-header in header)))))
|
||||
|
||||
(λ read-n [in len ?buffer]
|
||||
"read a string of exactly `len` characters from the `in` stream.
|
||||
If there aren't enough bytes, return nil"
|
||||
(local buffer (or ?buffer []))
|
||||
(if (<= len 0)
|
||||
(table.concat buffer)
|
||||
(match (in:read len)
|
||||
content
|
||||
(read-n in
|
||||
(- len (length content))
|
||||
(doto buffer (table.insert content))))))
|
||||
|
||||
(λ read-content [in header]
|
||||
"Reads the content of a JSON-RPC message given the header"
|
||||
(read-n in (tonumber header.Content-Length)))
|
||||
|
||||
(λ read [in]
|
||||
"Reads and parses a JSON-RPC message from the input stream
|
||||
Returns a table with the message if it succeeded, or a string with the parse error if it fails."
|
||||
(let [(_success? result)
|
||||
(-?>> (read-header in)
|
||||
(read-content in)
|
||||
(pcall decode))]
|
||||
result))
|
||||
|
||||
|
||||
(λ write [out msg]
|
||||
"Serializes and writes a JSON-RPC message to the given output stream"
|
||||
(let [content (encode msg)
|
||||
msg-stringified (.. "Content-Length: " (length content) "\r\n\r\n" content)]
|
||||
(out:write msg-stringified)
|
||||
(when out.flush
|
||||
(out:flush))))
|
||||
|
||||
{: read
|
||||
: write}
|
||||
20
src/fennel-ls/log.fnl
Normal file
20
src/fennel-ls/log.fnl
Normal file
@ -0,0 +1,20 @@
|
||||
"Log
|
||||
In the Language Server Protocol, io.stdout is used to send messages to the client.
|
||||
Because of this, I need another way to do print-debugging."
|
||||
|
||||
(local fennel (require :fennel))
|
||||
(local disable-logs false)
|
||||
(if disable-logs
|
||||
{:log #nil}
|
||||
(let [logdocument (io.open "/tmp/fennel.log" "w")]
|
||||
(assert logdocument)
|
||||
(fn log [...]
|
||||
(let [args []]
|
||||
(for [i 1 (select :# ...)]
|
||||
(table.insert args
|
||||
(let [item (select i ...)]
|
||||
(match (values item (type item))
|
||||
(str :string) str
|
||||
?any (fennel.view ?any)))))
|
||||
(logdocument:write (table.concat args) "\n")))
|
||||
{: log}))
|
||||
50
src/fennel-ls/message.fnl
Normal file
50
src/fennel-ls/message.fnl
Normal file
@ -0,0 +1,50 @@
|
||||
"Message
|
||||
Here are all the constructors for the various JSON-RPC responses
|
||||
that may need to be sent to the client.
|
||||
|
||||
I have them all here because I have a feeling I am conflating missing fields with null fields,
|
||||
and I want to have one location to look to fix this in the future."
|
||||
|
||||
(local error-codes
|
||||
{;; JSON-RPC errors
|
||||
:ParseError -32700
|
||||
:InvalidRequest -32600
|
||||
:MethodNotFound -32601
|
||||
:InvalidParams -32602
|
||||
:InternalError -32603
|
||||
;; LSP errors
|
||||
:ServerNotInitialized -32002
|
||||
:UnknownErrorCode -32001
|
||||
:RequestFailed -32802 ;; when the server has no excuse for failure
|
||||
:ServerCancelled -32802
|
||||
:ContentModified -32801 ;; I don't think this one is useful unless we do async things
|
||||
:RequestCancelled -32800}) ;; I don't think I'm going to even support cancelling things, that sounds like a pain
|
||||
|
||||
(λ create-error [code message ?id ?data]
|
||||
{:jsonrpc "2.0"
|
||||
:id ?id
|
||||
:error {:code (or (. error-codes code) code)
|
||||
:data ?data
|
||||
: message}})
|
||||
|
||||
(λ create-request [id method ?params]
|
||||
{:jsonrpc "2.0"
|
||||
: id
|
||||
: method
|
||||
:params ?params})
|
||||
|
||||
(λ create-notification [method ?params]
|
||||
{:jsonrpc "2.0"
|
||||
: method
|
||||
:params ?params})
|
||||
|
||||
(λ create-response [id ?result]
|
||||
{:jsonrpc "2.0"
|
||||
: id
|
||||
:result ?result})
|
||||
|
||||
{: create-notification
|
||||
: create-request
|
||||
: create-response
|
||||
: create-error}
|
||||
|
||||
30
src/fennel-ls/mod.fnl
Normal file
30
src/fennel-ls/mod.fnl
Normal file
@ -0,0 +1,30 @@
|
||||
"Mod
|
||||
This file has all the logic needed to take the name of a module and find the corresponding URI.
|
||||
I suspect this file is going to be gone after a bit of refactoring."
|
||||
|
||||
(local fennel (require :fennel))
|
||||
(local stringx (require :pl.stringx))
|
||||
(local plpath (require :pl.path))
|
||||
(local util (require :fennel-ls.util))
|
||||
|
||||
"works on my machine >:)"
|
||||
(local luapath "?.lua;src/?.lua")
|
||||
(local fennelpath "?.fnl;src/?.fnl")
|
||||
|
||||
(fn add-workspaces-to-path [path ?workspaces]
|
||||
(let [paths (stringx.split path ";")
|
||||
result []]
|
||||
(each [_ path (ipairs paths)]
|
||||
(if (plpath.isabs path)
|
||||
(table.insert result path)
|
||||
(each [_ space (ipairs (or ?workspaces []))]
|
||||
(table.insert result (plpath.normpath (plpath.join (util.uri->path space) path))))))
|
||||
(table.concat result ";")))
|
||||
|
||||
(fn lookup [{: root-uri} mod]
|
||||
(match (or (fennel.searchModule mod (add-workspaces-to-path luapath [root-uri]))
|
||||
(fennel.searchModule mod (add-workspaces-to-path fennelpath [root-uri])))
|
||||
modname (util.path->uri modname)
|
||||
nil nil))
|
||||
|
||||
{: lookup}
|
||||
37
src/fennel-ls/parser.fnl
Normal file
37
src/fennel-ls/parser.fnl
Normal file
@ -0,0 +1,37 @@
|
||||
(local fennel (require :fennel))
|
||||
(local util (require :fennel-ls.util))
|
||||
|
||||
(fn get-ast-info [ast info]
|
||||
"find a given key of info from an AST object"
|
||||
(or (. (getmetatable ast) info)
|
||||
(. ast info)))
|
||||
|
||||
(fn contains? [ast byte]
|
||||
"check if a byte is in range of the AST object"
|
||||
(and (= (type ast) :table)
|
||||
(<= (get-ast-info ast :bytestart)
|
||||
byte
|
||||
(get-ast-info ast :byteend))))
|
||||
|
||||
(fn past? [ast byte]
|
||||
"check if a byte is past the range of the AST object"
|
||||
(and (= (type ast) :table)
|
||||
(< byte (get-ast-info ast :bytestart))))
|
||||
|
||||
(fn range [ast]
|
||||
"create a LSP range representing the span of an AST object"
|
||||
(if (= (type ast) :table)
|
||||
(match (values (get-ast-info ast :bytestart) (get-ast-info ast :byteend))
|
||||
(i j)
|
||||
(let [(start-line start-col) (util.byte->pos i)
|
||||
(end-line end-col) (util.byte->pos j)]
|
||||
{:start {:line start-line :character start-col}
|
||||
:end {:line end-line :character end-col}}))))
|
||||
|
||||
(fn from-fennel [file]
|
||||
(icollect [k v (fennel.parser file.text file.uri)]
|
||||
v))
|
||||
|
||||
{: from-fennel
|
||||
: contains?
|
||||
: past?}
|
||||
52
src/fennel-ls/state.fnl
Normal file
52
src/fennel-ls/state.fnl
Normal file
@ -0,0 +1,52 @@
|
||||
(local util (require :fennel-ls.util))
|
||||
(local mod (require :fennel-ls.mod))
|
||||
|
||||
(λ analyze [] "TODO")
|
||||
|
||||
(λ init-state [self params]
|
||||
(set self.files {})
|
||||
(set self.modules {})
|
||||
(set self.root-uri params.rootUri))
|
||||
|
||||
(λ read-file [uri]
|
||||
(with-open [fd (io.open (util.uri->path uri))]
|
||||
{:uri uri
|
||||
:text (fd:read :*a)}))
|
||||
|
||||
(λ get-by-uri [self uri]
|
||||
(or (. self.files uri)
|
||||
(let [file (read-file uri)]
|
||||
(analyze file)
|
||||
(tset self.files uri file)
|
||||
file)))
|
||||
|
||||
(λ get-by-module [self module]
|
||||
(match (. self.modules module)
|
||||
uri (or (get-by-uri self uri)
|
||||
;; if the cached uri isn't found, clear the cache and try again
|
||||
(do (tset self.modules module nil)
|
||||
(get-by-module self module)))
|
||||
nil (let [uri (mod.lookup self module)]
|
||||
(tset self.modules module uri)
|
||||
(get-by-uri self uri))))
|
||||
|
||||
(λ set-uri-contents [self uri text]
|
||||
(if (. self.files uri)
|
||||
;; modify existing file
|
||||
(let [file (. self.files uri)]
|
||||
(when (not= text file.text)
|
||||
(set file.text text)
|
||||
(analyze file)
|
||||
file))
|
||||
;; create new file
|
||||
(let [file {: uri : text}]
|
||||
(tset self.files uri file)
|
||||
(analyze file)
|
||||
file)))
|
||||
|
||||
|
||||
|
||||
{: get-by-uri
|
||||
: get-by-module
|
||||
: set-uri-contents
|
||||
: init-state}
|
||||
128
src/fennel-ls/the-actual-code.fnl
Normal file
128
src/fennel-ls/the-actual-code.fnl
Normal file
@ -0,0 +1,128 @@
|
||||
"The actual code
|
||||
You finally made it. Here is the main code that implements the language server protocol
|
||||
|
||||
Every time the client sends a message, it gets handled by a function in the corresponding table type.
|
||||
(ie, a textDocument/didChange notification will call notifications.textDocument/didChange
|
||||
and a textDocument/defintion request will call requests.textDocument/didChange)"
|
||||
(local fennel (require :fennel))
|
||||
(local dir (require :pl.dir))
|
||||
|
||||
(local parser (require :fennel-ls.parser))
|
||||
(local util (require :fennel-ls.util))
|
||||
(local mod (require :fennel-ls.mod))
|
||||
(local {: log} (require :fennel-ls.log))
|
||||
(local state (require :fennel-ls.state))
|
||||
|
||||
(local requests [])
|
||||
(local notifications [])
|
||||
|
||||
(local capabilities
|
||||
{:textDocumentSync 1 ;; FIXME: upgrade to 2
|
||||
;; :notebookDocumentSync nil
|
||||
;; :completionProvider nil
|
||||
;; :hoverProvider nil
|
||||
;; :signatureHelpProvider nil
|
||||
;; :declarationProvider nil
|
||||
:definitionProvider {:workDoneProgress false}})
|
||||
;; :typeDefinitionProvider nil
|
||||
;; :implementationProvider nil
|
||||
;; :referencesProvider nil
|
||||
;; :documentHighlightProvider nil
|
||||
;; :documentSymbolProvider nil
|
||||
;; :codeActionProvider nil
|
||||
;; :codeLensProvider nil
|
||||
;; :documentLinkProvider nil
|
||||
;; :colorProvider nil
|
||||
;; :documentFormattingProvider nil
|
||||
;; :documentRangeFormattingProvider nil
|
||||
;; :documentOnTypeFormattingProvider nil
|
||||
;; :renameProvider nil
|
||||
;; :foldingRangeProvider nil
|
||||
;; :executeCommandProvider nil
|
||||
;; :selectionRangeProvider nil
|
||||
;; :linkedEditingRangeProvider nil
|
||||
;; :callHierarchyProvider nil
|
||||
;; :semanticTokensProvider nil
|
||||
;; :monikerProvider nil
|
||||
;; :typeHierarchyProvider nil
|
||||
;; :inlineValueProvider nil
|
||||
;; :inlayHintProvider nil
|
||||
;; :diagnosticProvider {:workDoneProgress false}})
|
||||
;; :workspaceSymbolProvider nil
|
||||
;; :workspace {:workspaceFolders nil
|
||||
;; :documentOperations {:didCreate nil
|
||||
;; :willCreate nil
|
||||
;; :didRename nil
|
||||
;; :willRename nil
|
||||
;; :didDelete nil
|
||||
;; :willDelete nil}})
|
||||
|
||||
(λ requests.initialize [self send params]
|
||||
(state.init-state self params)
|
||||
{:capabilities capabilities
|
||||
:serverInfo {:name "fennel-ls" :version "0.0.0"}})
|
||||
|
||||
(fn string? [j]
|
||||
(= (type j) :string))
|
||||
|
||||
(local require* (fennel.sym :require))
|
||||
(local local* (fennel.sym :local))
|
||||
(λ requests.textDocument/definition [self send {: position :textDocument {: uri}}]
|
||||
(local file (state.get-by-uri self uri))
|
||||
|
||||
(set file.ast (or file.ast
|
||||
(parser.from-fennel (. self.files uri))))
|
||||
(local ast file.ast)
|
||||
|
||||
(local byte (util.pos->byte file.text position.line position.character))
|
||||
|
||||
(var result nil)
|
||||
(λ check [ast]
|
||||
(log (fennel.view ast))
|
||||
(each [_ item (ipairs ast) :until (or result (parser.past? item byte))]
|
||||
(log (fennel.view ast))
|
||||
(if (parser.contains? item byte)
|
||||
(match item
|
||||
(where [require* module &as l]
|
||||
(and (fennel.list? l)
|
||||
(string? module)))
|
||||
(set result module)
|
||||
(where [local* _ [require* module &as l1] &as l2]
|
||||
(and (fennel.list? l1)
|
||||
(fennel.list? l2)
|
||||
(string? module)))
|
||||
(set result module)
|
||||
(where obj (fennel.list? obj))
|
||||
(check obj)))))
|
||||
|
||||
(check ast)
|
||||
|
||||
(if result
|
||||
{:uri (mod.lookup self result)
|
||||
:range {:start {:line 0 :character 0}
|
||||
:end {:line 0 :character 0}}}))
|
||||
|
||||
|
||||
(λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}]
|
||||
(local file (state.get-by-uri self uri))
|
||||
(assert file.open?)
|
||||
(util.apply-changes (. self.files uri) contentChanges))
|
||||
|
||||
(λ notifications.textDocument/didOpen [self send {:textDocument {: languageId : text : uri}}]
|
||||
(local file (state.set-uri-contents self uri text))
|
||||
(set file.open? true))
|
||||
|
||||
(λ notifications.textDocument/didClose [self send {:textDocument {: uri}}]
|
||||
(local file (state.get-by-uri self uri))
|
||||
(set file.open? false))
|
||||
|
||||
(λ requests.shutdown [self send]
|
||||
"The server still needs to respond to this request, so the program can't close yet. Wait until notifications.exit"
|
||||
nil)
|
||||
|
||||
(λ notifications.exit [self]
|
||||
(os.exit 0))
|
||||
|
||||
{: requests
|
||||
: notifications}
|
||||
|
||||
74
src/fennel-ls/util.fnl
Normal file
74
src/fennel-ls/util.fnl
Normal file
@ -0,0 +1,74 @@
|
||||
"Util
|
||||
A collection of utility functions. Many of these convert data between a
|
||||
Language-Server-Protocol representation and a Lua representation.
|
||||
These functions are all pure functions, which makes me happy."
|
||||
|
||||
(local {: startswith} (require :pl.stringx))
|
||||
|
||||
(λ uri->path [uri]
|
||||
(local prefix "file://")
|
||||
(assert (startswith uri prefix))
|
||||
(string.sub uri (+ (length prefix) 1)))
|
||||
|
||||
(λ path->uri [path]
|
||||
(.. "file://" path))
|
||||
|
||||
(λ next-line [str ?from]
|
||||
"Find the start of the next line from a given byte offset, or from the start of the string."
|
||||
(let [from (or ?from 1)]
|
||||
(match (str:find "[\r\n]" from)
|
||||
i (+ i (length (str:match "\r?\n?" i)))
|
||||
nil nil)))
|
||||
|
||||
(λ pos->byte [str line col]
|
||||
"convert a 0-indexed line and column into a 1-indexed byte. Doesn't yet handle UTF8 UTF16 magic from the protocol"
|
||||
(var sofar 1)
|
||||
(for [i 1 line :until (not sofar)]
|
||||
(set sofar (next-line str sofar)))
|
||||
(if sofar
|
||||
(+ sofar col)
|
||||
nil))
|
||||
|
||||
(λ byte->pos [str byte]
|
||||
"convert a 1-indexed byte into a 0-indexed line and column. Doesn't yet handle UTF8 UTF16 magic from the protocol"
|
||||
(local up-to (str:sub 1 (- byte 1)))
|
||||
(var lines 0)
|
||||
(var pos 1)
|
||||
(var prev nil)
|
||||
(while (do (set prev pos)
|
||||
(set pos (next-line up-to pos))
|
||||
pos)
|
||||
(set lines (+ 1 lines)))
|
||||
(values lines (+ (length up-to) (- prev) 1)))
|
||||
|
||||
(λ replace [text start-line start-col end-line end-col replacement]
|
||||
"Replaces a range of text with a replacement, using the protocol's definition of range. Doesn't yet handle UTF8 UTF16 magic from the protocol"
|
||||
(let [start (pos->byte text start-line start-col)
|
||||
end (pos->byte text end-line end-col)]
|
||||
(..
|
||||
(text:sub 1 (- start 1))
|
||||
replacement
|
||||
(text:sub end))))
|
||||
|
||||
(λ apply-changes [initial-text contentChanges]
|
||||
"Take's a list of Language-Server-Protocol contentChanges and applies them to a piece of text. Doesn't yet handle UTF8 UTF16 magic from the protocol"
|
||||
(accumulate [contents initial-text
|
||||
_ change (ipairs contentChanges)]
|
||||
(match change
|
||||
;; Handle a change
|
||||
{:range {: start : end} : text}
|
||||
(replace contents
|
||||
start.line
|
||||
start.character
|
||||
end.line
|
||||
end.character
|
||||
text)
|
||||
;; A replacment of the entire body
|
||||
{: text}
|
||||
text)))
|
||||
|
||||
{: uri->path
|
||||
: path->uri
|
||||
: pos->byte
|
||||
: byte->pos
|
||||
: apply-changes}
|
||||
Loading…
Reference in New Issue
Block a user