From 1e3edf800d65ec9adb798b9b1c0ffdcce653a859 Mon Sep 17 00:00:00 2001 From: XeroOl Date: Wed, 16 Apr 2025 21:28:56 -0500 Subject: [PATCH] replace completions with a completely new completions.fnl implementation the tests are failing now, because it's generating completions differently, with no documentation or completion types, but it seems like its working well. --- src/fennel-ls/analyzer.fnl | 10 +-- src/fennel-ls/completions.fnl | 96 +++++++++++++++++++++++++ src/fennel-ls/config.fnl | 7 +- src/fennel-ls/handlers.fnl | 132 +--------------------------------- 4 files changed, 104 insertions(+), 141 deletions(-) create mode 100644 src/fennel-ls/completions.fnl diff --git a/src/fennel-ls/analyzer.fnl b/src/fennel-ls/analyzer.fnl index eb14db7..c116eda 100644 --- a/src/fennel-ls/analyzer.fnl +++ b/src/fennel-ls/analyzer.fnl @@ -206,7 +206,7 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find (λ search-name-and-scope [server file name scope ?opts] "find a definition just from the name of the item, and the scope it is in" - (assert (= (type name) :string)) + (assert (= (type name) :string) "search-name-and-scope needs a string") (let [split (utils.multi-sym-split name) stack (stack-add-split! [] split) opts (or ?opts {})] @@ -257,15 +257,15 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find _ child (ipairs ast) &until result] (if (contains? child byte) - (recurse child byte))) + (recurse child byte))) (and (not (sym? ast)) (not (varg? ast))) (accumulate [(result _parent) nil key value (pairs ast) &until result] (if (contains? key byte) - (recurse key byte) - (contains? value byte) - (recurse value byte))))))) + (recurse key byte) + (contains? value byte) + (recurse value byte))))))) (values (accumulate [result nil _ top-level-form (ipairs ast) &until result] (if (contains? top-level-form byte) diff --git a/src/fennel-ls/completions.fnl b/src/fennel-ls/completions.fnl new file mode 100644 index 0000000..8745159 --- /dev/null +++ b/src/fennel-ls/completions.fnl @@ -0,0 +1,96 @@ +(local files (require :fennel-ls.files)) +(local utils (require :fennel-ls.utils)) +(local analyzer (require :fennel-ls.analyzer)) +(local docs (require :fennel-ls.docs)) +(local fennel (require :fennel)) +(local message (require :fennel-ls.message)) +(local format (require :fennel-ls.formatter)) + +;; CompletionItemKind +(local _kinds + {:Text 1 :Method 2 :Function 3 :Constructor 4 :Field 5 :Variable 6 :Class 7 + :Interface 8 :Module 9 :Property 10 :Unit 11 :Value 12 :Enum 13 :Keyword 14 + :Snippet 15 :Color 16 :File 17 :Reference 18 :Folder 19 :EnumMember 20 + :Constant 21 :Struct 22 :Event 23 :Operator 24 :TypeParameter 25}) + +(λ textDocument/completion [server _send {: position :textDocument {: uri}}] + ;; get the file + (let [file (files.get-by-uri server uri) + ;; find where the cursor is + byte (utils.position->byte file.text position server.position-encoding) + ;; find what ast objects are under the cursor + (?symbol parents) (analyzer.find-symbol file.ast byte) + ;; check what context I'm in + in-call-position? (and (fennel.list? (. parents 1)) + (= ?symbol (. parents 1 1))) + ;; find the first one that contains a scope + scope (or (accumulate [?find nil _ parent (ipairs parents) &until ?find] + (. file.scopes parent)) + file.scope) + range (if ?symbol (message.ast->range server file ?symbol) {:start position :end position}) + results [] + seen []] + + (fn add-completion [definition name] + "add the completion. also recursively adds the fields' completions" + (when (not (. seen definition)) + (set (. seen definition) true) + (table.insert results + (doto (format.completion-item-format name definition) + (tset :filterText name) + (tset :textEdit {:newText name : range}))) + + (when (= (type definition.definition) :string) + (each [key value (pairs (-> (docs.get-global server :string) (. :fields)))] + (add-completion value (.. name ":" key)))) + (when (fennel.table? definition.definition) + (each [field value (pairs definition.definition)] + (when (= (type field) :string) + (case (analyzer.search-ast server definition.file value [] {}) + def (do + (when (or (?. def :metadata :fnl/arglist) + (and (fennel.list? def.definition) + (or (fennel.sym? (. def.definition 1) "fn")) + (or (fennel.sym? (. def.definition 1) "λ")))) + (add-completion def (.. name ":" field))) + (add-completion def (.. name "." field))) + _ (do + (io.stderr:write "BAD!!!! undocumented field: " (tostring field) "\n") + {:label field}))))) + (when definition.fields + (each [field value (pairs definition.fields)] + (when (= (type field) :string) + (add-completion value (.. name "." field))))) + + (set (. seen definition) false))) + ;; end yield + + (each [_ global* (ipairs file.allowed-globals)] + (case (analyzer.search-name-and-scope server file global* scope) + def (add-completion def global*) + _ (do + (io.stderr:write "BAD!!!! undocumented global: " (tostring global*) "\n") + {:label global*}))) + (var scope scope) + (while scope + (each [mangling (pairs scope.manglings)] + (case (analyzer.search-name-and-scope server file mangling scope) + def (add-completion def mangling) + _ (do + (io.stderr:write "BAD!!!! undocumented mangling: " (tostring mangling) "\n") + {:label mangling}))) + (when in-call-position? + (each [macro* (pairs scope.macros)] + ;; TODO make it work like the other ones + (table.insert results {:label macro* :filterText macro* :textEdit {:newText macro* : range}})) + + (each [special (pairs scope.specials)] + (case (analyzer.search-name-and-scope server file special scope) + def (add-completion def special) + _ (do + (io.stderr:write "BAD!!!! undocumented special: " (tostring special) "\n") + {:label special})))) + (set scope scope.parent)) + results)) + +{: textDocument/completion} diff --git a/src/fennel-ls/config.fnl b/src/fennel-ls/config.fnl index a41573f..55b5659 100644 --- a/src/fennel-ls/config.fnl +++ b/src/fennel-ls/config.fnl @@ -96,12 +96,7 @@ However, when not an option, fennel-ls will fall back to positionEncoding=\"utf- (set server.macro-modules {}) (set server.root-uri params.rootUri) (set server.position-encoding (choose-position-encoding params)) - (reload server) - ;; Eglot does completions differently than every other client I've seen so - ;; far, in that it considers foo.bar to be one "symbol". If the user types - ;; `foo.b`, every other client accepts `bar` as a completion, bun eglot wants - ;; the full `foo.bar` symbol. - (set server.EGLOT_COMPLETION_QUIRK_MODE (= (?. params :clientInfo :name) :Eglot))) + (reload server)) (λ validate [{: configuration} invalid] (when (not= :string (type configuration.fennel-path)) diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index 5e788dd..fd4a176 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -37,7 +37,7 @@ Every time the client sends a message, it gets handled by a function in the corr ;; :notebookDocumentSync nil :completionProvider {:workDoneProgress false :resolveProvider false - :triggerCharacters ["(" "[" "{" "." ":" "\""] + :triggerCharacters ["(" "[" "{"] :completionItem {:labelDetailsSupport false}} :hoverProvider {:workDoneProgress false} :signatureHelpProvider {:workDoneProgress false @@ -163,135 +163,7 @@ Every time the client sends a message, it gets handled by a function in the corr symbol)} (catch _ nil)))) -;; CompletionItemKind -(local kinds - {:Text 1 :Method 2 :Function 3 :Constructor 4 :Field 5 :Variable 6 :Class 7 - :Interface 8 :Module 9 :Property 10 :Unit 11 :Value 12 :Enum 13 :Keyword 14 - :Snippet 15 :Color 16 :File 17 :Reference 18 :Folder 19 :EnumMember 20 - :Constant 21 :Struct 22 :Event 23 :Operator 24 :TypeParameter 25}) - -;; All of the helper functions for textDocument/completion are here until I -;; finish refactoring them, and then they can find a home in analyzer.fnl -(λ field-completion-helper [server ?result last-found-binding] - (case ?result - {: definition : file} - (case (values definition (type definition)) - ;; fields of a string are hardcoded to "string" - (_str :string) (icollect [label info (pairs (. (docs.get-global server :string) :fields))] - (formatter.completion-item-format label info)) - ;; fields of a table - (where (tbl :table) (fennel.table? tbl)) (let [keys [] - result []] - (icollect [label _ (pairs tbl) &into keys] - label) - (when (?. last-found-binding 1 :fields) - (icollect [label _ (pairs (. last-found-binding 1 :fields)) &into keys] - label)) - (icollect [_ label (pairs keys) &into result] - (if (= (type label) :string) - (let [last-found-binding []] - (case (analyzer.search-ast server file tbl [label] {:save-last-binding last-found-binding}) - def (do - (icollect [_ completion (ipairs (or (field-completion-helper server def last-found-binding) [])) &into result] - (do - (set completion.filterText completion.label) - (set completion.insertText (.. label "." completion.label)) - (set completion.label completion.insertText) - completion)) - (formatter.completion-item-format label def)) - _ {: label :kind kinds.Field})))) - result)) - {: metadata : fields} - (let [_metadata metadata] - (icollect [label info (pairs fields)] - (formatter.completion-item-format label info))) - _ nil)) - -(λ collect-scope [scope typ server file ?target ?default-kind] - (let [result (or ?target [])] - (var scope scope) - (while scope - (icollect [name (pairs (. scope typ)) &into result] - (let [last-found-binding [] - item (case (analyzer.search-name-and-scope server file name scope {:save-last-binding last-found-binding}) - def (do - (icollect [_ completion (ipairs (or (field-completion-helper server def last-found-binding) [])) &into result] - (do - (set completion.filterText completion.label) - (set completion.insertText (.. name "." completion.label)) - (set completion.label completion.insertText) - completion)) - (formatter.completion-item-format name def)) - _ {:label name})] - (when (= nil item.kind) - (set item.kind ?default-kind)) - item)) - (set scope scope.parent)) - result)) - -(λ scope-completion [server file _byte ?symbol parents] - (let [scope (or (accumulate [result nil - _ parent (ipairs parents) - &until result] - (. file.scopes parent)) - file.scope) - ?parent (. parents 1) - result [] - in-call-position? (and (fennel.list? ?parent) - (= ?symbol (. ?parent 1)))] - (collect-scope scope :manglings server file result kinds.Variable) - - (when in-call-position? - (collect-scope scope :macros server file result kinds.Keyword) - (collect-scope scope :specials server file result kinds.Operator)) - (icollect [_ k (ipairs file.allowed-globals) &into result] - (let [last-found-binding []] - (case (analyzer.search-name-and-scope server file k scope {:save-last-binding last-found-binding}) - def (do (icollect [_ completion (ipairs (or (field-completion-helper server def last-found-binding) [])) &into result] - (do - (set completion.filterText completion.label) - (set completion.insertText (.. k "." completion.label)) - (set completion.label completion.insertText) - completion)) - (formatter.completion-item-format k def)) - _ {:label k}))))) - -(λ field-completion [server file symbol split] - (let [stack (fcollect [i (- (length split) 1) 2 -1] - (. split i)) - last-found-binding [] - result (analyzer.search-main server file symbol {:save-last-binding last-found-binding} {: stack})] - (field-completion-helper server result last-found-binding))) - -(λ requests.textDocument/completion [server _send {: position :textDocument {: uri}}] - (let [file (files.get-by-uri server uri) - byte (utils.position->byte file.text position server.position-encoding) - (?symbol parents) (analyzer.find-symbol file.ast byte)] - (case (-?> ?symbol utils.multi-sym-split) - - ;; completion from current scope - (where (or nil [_ nil])) - (let [input-range (if ?symbol (message.multisym->range server file ?symbol -1) {:start position :end position}) - ?completions (scope-completion server file byte ?symbol parents)] - (if ?completions - (let [?completions (utils.uniq-by ?completions #$.label)] - (each [_ completion (ipairs ?completions)] - (set completion.textEdit {:newText completion.label :range input-range})) - ?completions))) - - ;; completion from field - [_a _b &as split] - (let [input-range (message.multisym->range server file ?symbol -1) - ?completions (field-completion server file ?symbol split)] - (if ?completions - (if server.EGLOT_COMPLETION_QUIRK_MODE - (let [prefix (string.gsub (tostring ?symbol) "[^.:]*$" "")] - (each [_ completion (ipairs ?completions)] - (set completion.filterText (.. prefix completion.label)) - (set completion.insertText (.. prefix completion.label)))) - (each [_ completion (ipairs ?completions)] - (set completion.textEdit {:newText completion.label :range input-range})))) - ?completions)))) +(set requests.textDocument/completion (. (require :fennel-ls.completions) :textDocument/completion)) (λ requests.textDocument/rename [server _send {: position :textDocument {: uri} :newName new-name}] (let [file (files.get-by-uri server uri)