diff --git a/fennel-ls.fnl b/fennel-ls.fnl index 1bf023b..95a8c8d 100644 --- a/fennel-ls.fnl +++ b/fennel-ls.fnl @@ -1,86 +1,87 @@ -(local {: encode : decode} (require :json.json)) -(local {: split} (require :pl.stringx)) +(local requests []) +(local notifications []) -(local capabilities {:positionEncoding nil ; "utf-8" - :textDocumentSync nil - :notepookDocumentSync nil - :completionProvider nil - :hoverProvider nil - :signatureHelpProvider nil - :declarationProvider nil - :definitionProvider nil - :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 nil - :workspaceSymbolProvider nil - :workspace nil}) +(local capabilities + {:positionEncoding "utf-8" + :textDocumentSync nil + :notebookDocumentSync nil + :completionProvider nil + :hoverProvider nil + :signatureHelpProvider nil + :declarationProvider nil + :definitionProvider nil + :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 nil + :workspaceSymbolProvider nil + :workspace {:workspaceFolders nil + :fileOperations {:didCreate nil + :willCreate nil + :didRename nil + :willRename nil + :didDelete nil + :willDelete nil}}}) -(local handlers {}) +(λ requests.initialize [params] + {:capabilities capabilities + :serverInfo {:name "fennel-ls" :version "0.0.0"}}) -(λ handlers.initialize [params] - {:capabilities capabilities - :serverInfo {:name "fennel-ls" :version "0.0.0"}}) +(λ requests.shutdown []) + ;; no op -(λ receive-message [in] - (local header {}) - (while - (match (in:read) - "\r" false - header-line - (let [[k v] (split header-line ": " 2)] - (tset header k v) - true) - _ nil)) - (let [len (tonumber header.Content-Length) - buffer []] - (var sofar 0) - (while (< sofar len) - (let [r (in:read (- len sofar))] - (set sofar (+ sofar (length r))) - (table.insert buffer r))) - (decode (table.concat buffer)))) +(λ notifications.exit [] + (os.exit 0)) -(λ send-message [out msg] - (let [content (encode msg) - msg-stringified (.. "Content-Length: " (length content) "\r\n\r\n" content)] - (out:write msg-stringified))) +(λ handle-request [id method ?params] + (let [callback (. requests method) + result {: id :jsonrpc "2.0"}] + (if callback + (tset result :result (callback ?params)) + (tset result :error (.. "Unknown message type: " method))) + result)) -(λ handle [{: jsonrpc : method : params : id &as msg}] - (assert (= jsonrpc "2.0")) - ;; Right now, if the callback crashes, the whole server does. - ;; It would be nice to turn this into an error message - (match (. handlers method) - callback - (let [result (callback params)] - {: id - : method - :params result - :jsonrpc "2.0"}) - _ {: id - : method - :error (.. "unknown method " msg.method)})) +(λ handle-response [id result]) + ;; Do nothing -{: receive-message - : send-message - : handle} +(λ handle-bad-response [id err] + (error (.. "oopsie: " err.code))) + +(λ handle-notification [method ?params] + (let [callback (. notifications method)] + (if callback + (callback ?params)))) + +(λ handle [msg] + "The entry point for all messages." + (assert (= msg.jsonrpc "2.0") "Aha! You forgot to repeat that jsonrpc is version 2.0!") + (match msg + {: id : method :params ?params} (handle-request id method ?params) + {: method :params ?params} (handle-notification method ?params) + {: id : result} (handle-response id result) + {: id :error err} (handle-bad-response id err) + _ {:id msg.id + :error "I just received a message that doesn't make sense to me" + :jsonrpc "2.0"})) + +{: handle} diff --git a/lsp-io.fnl b/lsp-io.fnl new file mode 100644 index 0000000..7a38f8f --- /dev/null +++ b/lsp-io.fnl @@ -0,0 +1,44 @@ +(local {: encode : decode} (require :json.json)) +(local {: split} (require :pl.stringx)) + +(λ read-header [in ?header] + (let [header (or ?header {})] + (match (in:read) + ;; base cases + "\r" header + nil 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-content [in header] + (let [len (tonumber header.Content-Length) + buffer []] + ;; TODO make this code as tolerable as possible + (var sofar 0) + (var currently-read-bytes nil) + (while + (and (< sofar len) + (do (set currently-read-bytes (in:read (- len sofar))) + currently-read-bytes)) + (set sofar (+ sofar (length currently-read-bytes))) + (table.insert buffer currently-read-bytes)) + (decode (table.concat buffer)))) + + +(λ read-message [in] + (match (read-header in) + header (read-content in header) + nil nil)) + +(λ write-message [out msg] + (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-message + : write-message} diff --git a/main.fnl b/main.fnl index 89a8976..98e5264 100644 --- a/main.fnl +++ b/main.fnl @@ -1,20 +1,16 @@ -(local - {: handle - : send-message - : receive-message} - (require "fennel-ls")) +(local fennel (require :fennel)) +(local {: read-message : write-message} (require :lsp-io)) +(local {: handle} (require :fennel-ls)) + +(λ main-loop [in out] + (let [msg (read-message in)] + (when msg + (let [response (handle msg)] + (when response + (write-message out response)) + (main-loop in out))))) (λ main [] - (local in (io.input)) - (local out (io.output)) - (while - (match (receive-message in) - msg - (do - (let [response (handle msg)] - (if msg.id - (send-message out response))) - true) - _ nil))) + (main-loop (io.input) (io.output))) (main) diff --git a/test.fnl b/test.fnl index d73aaf8..a05f610 100644 --- a/test.fnl +++ b/test.fnl @@ -1,93 +1,43 @@ -(local fennel-ls (require :fennel-ls)) +(local {: handle} (require :fennel-ls)) +(local {: read-message : write-message} (require :lsp-io)) (local stringio (require :pl.stringio)) (local {: view} (require :fennel)) (local busted (require :busted)) ((require :busted.runner)) -(macro describe! [title ...] - `(busted.describe ,title (fn [] ,...))) - -(macro it! [title ...] - `(busted.it ,title (fn [] ,...))) +(macro it! [title ...] `(busted.it ,title (fn [] ,...))) +(macro describe! [title ...] `(busted.describe ,title (fn [] ,...))) (describe! "fennel-ls" (it! "parses incoming messages" (let [out (stringio.open "Content-Length: 29\r\n\r\n{\"my json content\":\"is cool\"}")] (assert.same {"my json content" "is cool"} - (fennel-ls.recieve-message out)))) + (read-message out)))) (it! "serializes outgoing messages" (let [in (stringio.create)] - (fennel-ls.send-message in {"my json content" "is cool"}) + (write-message in {"my json content" "is cool"}) (assert.same "Content-Length: 29\r\n\r\n{\"my json content\":\"is cool\"}" (in:value)))) + + (it! "can read multiple messages" + (let [out (stringio.open "Content-Length: 29\r\n\r\n{\"my json content\":\"is cool\"}Content-Length: 29\r\n\r\n{\"my json content\":\"is cool\"}")] + (assert.same {"my json content" "is cool"} + (read-message out)) + (assert.same {"my json content" "is cool"} + (read-message out)) + (assert.same nil + (read-message out)))) + (it! "responds to initialize" (local initialize-message {:id 1 :jsonrpc "2.0" :method "initialize" :params - {:capabilities {:callHierarchy {:dynamicRegistration false} - :textDocument {:codeAction {:codeActionLiteralSupport - {:codeActionKind - {:valueSet - ["" "Empty" "QuickFix" "Refactor" - "RefactorExtract" "RefactorInline" - "RefactorRewrite" "Source" - "SourceOrganizeImports" "quickfix" - "refactor" "refactor.extract" - "refactor.inline" "refactor.rewrite" - "source" "source.organizeImports"]}} - :dataSupport true - :dynamicRegistration false - :resolveSupport {:properties ["edit"]}} - :completion {:completionItem {:commitCharactersSupport true - :deprecatedSupport true - :documentationFormat ["markdown" "plaintext"] - :insertReplaceSupport true - :labelDetailsSupport true - :preselectSupport true - :resolveSupport {:properties ["documentation" "detail" "additionalTextEdits"]} - :snippetSupport true - :tagSupport {:valueSet [1]}} - :completionItemKind {:valueSet [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25]} - :contextSupport false - :dynamicRegistration false} - :declaration {:linkSupport true} - :definition {:linkSupport true} - :documentHighlight {:dynamicRegistration false} - :documentSymbol {:dynamicRegistration false - :hierarchicalDocumentSymbolSupport true - :symbolKind {:valueSet [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26]}} - :hover {:contentFormat ["markdown" "plaintext"] - :dynamicRegistration false} - :implementation {:linkSupport true} - :publishDiagnostics {:relatedInformation true - :tagSupport {:valueSet [1 2]}} - :references {:dynamicRegistration false} - :rename {:dynamicRegistration false - :prepareSupport true} - :signatureHelp {:dynamicRegistration false - :signatureInformation {:activeParameterSupport true - :documentationFormat ["markdown" "plaintext"] - :parameterInformation {:labelOffsetSupport true}}} - :synchronization {:didSave true - :dynamicRegistration false - :willSave false - :willSaveWaitUntil false} - :typeDefinition {:linkSupport true}} - :window {:showDocument {:support false} - :showMessage {:messageActionItem {:additionalPropertiesSupport false}} - :workDoneProgress true} - :workspace {:applyEdit true - :configuration true - :symbol {:dynamicRegistration false - :hierarchicalWorkspaceSymbolSupport true - :symbolKind {:valueSet [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26]}} - :workspaceEdit {:resourceOperations ["rename" "create" "delete"]} - :workspaceFolders true}} + {:capabilities {} :clientInfo {:name "Neovim" :version "0.7.2"} :initializationOptions {} :processId 16245 @@ -96,4 +46,12 @@ :trace "off" :workspaceFolders [{:name "/home/xerool/Documents/projects/fennel-ls" :uri "file:///home/xerool/Documents/projects/fennel-ls"}]}}) - (local result (fennel-ls.handle initialize-message)))) + (assert + (match (handle initialize-message) + {:id 1 + :jsonrpc "2.0" + :result {:capabilities {} + :serverInfo {:name "fennel-ls" : version}}} + true + otherwise (values false (view otherwise)))))) +