diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index dcc6cbb..b013e8e 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -64,6 +64,7 @@ Every time the client sends a message, it gets handled by a function in the corr (λ requests.initialize [self send params] (state.init-state self params) {:capabilities capabilities + :positionEncoding self.position-encoding :serverInfo {:name "fennel-ls" :version "0.0.0"}}) (λ requests.textDocument/definition [self send {: position :textDocument {: uri}}] diff --git a/src/fennel-ls/state.fnl b/src/fennel-ls/state.fnl index 6189e43..bfa00b3 100644 --- a/src/fennel-ls/state.fnl +++ b/src/fennel-ls/state.fnl @@ -100,10 +100,27 @@ in the \"self\" object." (λ make-configuration [?c] (make-configuration-from-template default-configuration ?c)) +(λ choose-position-encoding [init-params] + "fennel-ls natively uses utf-8, so ideally we will choose positionEncoding=utf-8. +However, fennel-ls can fall back to positionEncoding=utf-16 (with a performance hit)." + (let [?position-encodings (?. init-params :capabilities :general :positionEncodings) + utf8? + (if (= (type ?position-encodings) :table) + (accumulate [utf-8? false + _ encoding (ipairs ?position-encodings)] + (or (= encoding :utf-8) + (= encoding :utf8))) + false)] + (if utf8? + :utf-8 + :utf-16))) + + (λ init-state [self params] (set self.files {}) (set self.modules {}) (set self.root-uri params.rootUri) + (set self.position-encoding (choose-position-encoding params)) (set self.configuration (make-configuration))) (λ write-configuration [self ?configuration] diff --git a/test/capabilities-test.fnl b/test/capabilities-test.fnl new file mode 100644 index 0000000..521445a --- /dev/null +++ b/test/capabilities-test.fnl @@ -0,0 +1,61 @@ +(import-macros {: is-matching : is-casing : describe : it : before-each} :test) +(local {: view} (require :fennel)) + +(local is (require :test.is)) +(local {: ROOT-URI + : ROOT-PATH + : create-client} (require :test.mock-client)) + +(fn default [tbl field value] + (when (= nil (. tbl field)) + (tset tbl field value))) + +(fn client-initialization [params] + (default params :clientInfo {:name "xerool's mock client" :version "9000"}) ;; not necessary, but why not have some fun? + (default params :rootPath ROOT-PATH) ;; deprecated, TODO delete + (default params :rootUri ROOT-URI) ;; deprecated, TODO delete + {default params :workspaceFolders [{:name "my cool space" :uri ROOT-URI}]} + (default params :capabilities {}) + (default params :trace "off") ;; | "messages" | "verbose" + ;; :initializationOptions {}) ;; LspAny + ;; :processId nil + ;; :locale "en" ;; I don't support languages/translations as of now + params) + +(describe "capabilities negotiations" + + ; (it "chooses utf-16" + ; (let [(self [response]) + ; (create-client + ; {:params + ; (client-initialization + ; {:capabilities + ; {:general + ; {:positionEncodings + ; [:utf-16]}}})})] + ; (is.equal :utf-16 (. response :result :positionEncoding)) + ; (self:open-file! "foo.fnl" "(let [𐐀𐐀 100] 𐐀𐐀)") + ; (let [[response] (self:definition "foo.fnl" 0 16)] + ; (is.equal 6 response.result.range.start.character) + ; (is.equal 10 response.result.range.end.character)))) + + (it "chooses utf-8 if at all possible" + (let [(self [response]) + (create-client + {:params + (client-initialization + {:capabilities + {:general + {:positionEncodings + [:utf-16 :utf-8]}}})})] + (is.equal :utf-8 (. response :result :positionEncoding)) + (self:open-file! "foo.fnl" "(let [𐐀𐐀 100] 𐐀𐐀)") + (let [[response] (self:definition "foo.fnl" 0 20)] + (is.equal 6 response.result.range.start.character) + (is.equal 14 response.result.range.end.character))))) + +(it "falls back to utf-16" + (let [(self [response]) (create-client {:params (client-initialization {})})] + (is.equal :utf-16 (. response :result :positionEncoding)))) + + diff --git a/test/init.fnl b/test/init.fnl index d37d954..b2c88e6 100644 --- a/test/init.fnl +++ b/test/init.fnl @@ -1,3 +1,4 @@ +(require :test.capabilities-test) (require :test.completion-test) (require :test.diagnostic-test) (require :test.goto-definition-test) diff --git a/test/lust.lua b/test/lust.lua index 73216c3..9c7cf05 100644 --- a/test/lust.lua +++ b/test/lust.lua @@ -64,7 +64,7 @@ function lust.it(name, fn) end end end - + if has(arg, "--quit-at-first-test") then os.exit(1) end end diff --git a/test/mock-client.fnl b/test/mock-client.fnl index ac29897..010db0c 100644 --- a/test/mock-client.fnl +++ b/test/mock-client.fnl @@ -6,14 +6,11 @@ (: :read :*a) (: :sub 1 -2) ;; take off newline (.. "/test/test-project"))) + (local ROOT-URI (.. "file://" ROOT-PATH)) -(local initialization-message - {:id 1 - :jsonrpc "2.0" - :method "initialize" - :params +(local default-params {:capabilities {} :clientInfo {:name "Neovim" :version "0.7.2"} :initializationOptions {} @@ -22,17 +19,23 @@ :rootUri ROOT-URI :trace "off" :workspaceFolders [{:name ROOT-PATH - :uri ROOT-URI}]}}) + :uri ROOT-URI}]}) (local mt {}) -(fn create-client [?config] - (let [self (doto {:server [] :prev-id 1} (setmetatable mt))] - (dispatch.handle* self.server initialization-message) - (if ?config - (dispatch.handle* self.server {:jsonrpc "2.0" - :method :workspace/didChangeConfiguration - :params {:settings ?config}})) - self)) +(fn create-client [?opts] + (let [self (doto {:server [] :prev-id 1} (setmetatable mt)) + initialize {:id 1 + :jsonrpc "2.0" + :method "initialize" + :params (or (?. ?opts :params) default-params)} + result (dispatch.handle* self.server initialize)] + (case (?. ?opts :settings) + settings + (dispatch.handle* self.server + {:jsonrpc "2.0" + :method :workspace/didChangeConfiguration + :params {: settings}})) + (values self result))) (fn next-id! [self] (set self.prev-id (+ self.prev-id 1)) @@ -80,4 +83,5 @@ : references}) {: create-client - : ROOT-URI} + : ROOT-URI + : ROOT-PATH} diff --git a/test/references-test.fnl b/test/references-test.fnl index 1658b82..fcd3db5 100644 --- a/test/references-test.fnl +++ b/test/references-test.fnl @@ -26,6 +26,22 @@ (check-references "(let [x 10] x)" 0 6 [{:uri filename :range (message.pos->range 0 12 0 13)}])) + (let [x 10] x x x) + (it "finds multiple reference from let" + (check-references "(let [x 10] x x x)" 0 6 + [{:uri filename :range (message.pos->range 0 12 0 13)} + {:uri filename :range (message.pos->range 0 14 0 15)} + {:uri filename :range (message.pos->range 0 16 0 17)}])) + + (it "finds a reference from fn" + (check-references "(fn x []) x" 0 10 + [{:uri filename :range (message.pos->range 0 10 0 11)}])) + + ; (it "finds a reference from fn" + ; (check-references "(fn x []) x" 0 4 + ; [{:uri filename :range (message.pos->range 0 10 0 11)}])) + + (it "doesn't crash here" (check-references "(let [x nil] x.y)" 0 14 nil))) diff --git a/test/settings-test.fnl b/test/settings-test.fnl index ddca2bc..d505af3 100644 --- a/test/settings-test.fnl +++ b/test/settings-test.fnl @@ -1,12 +1,13 @@ (import-macros {: is-matching : describe : it : before-each} :test) (local is (require :test.is)) +(local {: view} (require :fennel)) (local {: ROOT-URI : create-client} (require :test.mock-client)) (describe "settings" (it "can set the path" - (let [client (doto (create-client {:fennel-ls {:fennel-path "./?/?.fnl"}}) + (let [client (doto (create-client {:settings {:fennel-ls {:fennel-path "./?/?.fnl"}}}) (: :open-file! (.. ROOT-URI :/test.fnl) "(local {: this-is-in-modname} (require :modname))")) result (client:definition (.. ROOT-URI :/test.fnl) 0 12)] (is-matching @@ -15,9 +16,9 @@ "error message"))) (it "can set the macro path" - (let [client (create-client {:fennel-ls {:macro-path "./?/?.fnl"}}) + (let [client (create-client {:settings {:fennel-ls {:macro-path "./?/?.fnl"}}}) responses (client:open-file! (.. ROOT-URI :/test.fnl) "(import-macros {: this-is-in-modname} :modname)")] - (assert (not (. responses 1 :params 1)) "if the import-macros fails it generates a diagnostic (for now at least)"))) + (assert (not (. responses 1 :params :diagnostics 1)) "if the import-macros fails it generates a diagnostic (for now at least)"))) ;; (it "recompiles modules if the macro files are modified)" @@ -38,7 +39,7 @@ ;; I suspect this test will fail when I put warnings for module return type (it "can disable some lints" - (let [client (create-client {:fennel-ls {:checks {:unused-definition false}}}) + (let [client (create-client {:settings {:fennel-ls {:checks {:unused-definition false}}}}) responses (client:open-file! (.. ROOT-URI :/test.fnl) "(local x 10)")] (is-matching responses [{:method :textDocument/publishDiagnostics