~xerool/fennel-ls

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
6 3

[PATCH 1/3] Fix crash when requesting signature help at the top level.

Details
Message ID
<20250322110509.38672-1-micampe@micampe.it>
Sender timestamp
1742645107
DKIM signature
pass
Download raw message
Patch: +3 -2
diff --git a/src/fennel-ls/analyzer.fnl b/src/fennel-ls/analyzer.fnl
index 89fabac..be286a9 100644
--- a/src/fennel-ls/analyzer.fnl
+++ b/src/fennel-ls/analyzer.fnl
@@ -278,8 +278,9 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find
  "Find the nearest call

returns the called symbol and the argument number position points to"
  (let [(_ [[call] [parent]]) (find-symbol file.ast byte)]
    (if (special? parent)
  (let [(_ [[call] parent-call]) (find-symbol file.ast byte)
        parent (?. parent-call 1)]
    (if (and parent (special? parent))
        (values parent -1)
        (values call -1))))

-- 
2.39.5 (Apple Git-154)

[PATCH 2/3] Update diagnostics to be spec compliant.

Details
Message ID
<20250322110509.38672-2-micampe@micampe.it>
In-Reply-To
<20250322110509.38672-1-micampe@micampe.it> (view parent)
Sender timestamp
1742645108
DKIM signature
pass
Download raw message
Patch: +49 -62
The spec says diagnostic.codeDescription should be an object with an URI
linking to information about the error, we were using it as a string
identifier for the diagnostic.

This was making Helix ignore all diagnostics from fennel-ls.

Changed diagnostic.code to be the string identifier, removed numeric
identifiers and codeDescription field.
---
 src/fennel-ls/lint.fnl    | 38 ++++++++-------------
 src/fennel-ls/message.fnl |  4 +--
 test/lint.fnl             | 69 +++++++++++++++++++--------------------
 3 files changed, 49 insertions(+), 62 deletions(-)

diff --git a/src/fennel-ls/lint.fnl b/src/fennel-ls/lint.fnl
index 1af180f..3827abe 100644
--- a/src/fennel-ls/lint.fnl
+++ b/src/fennel-ls/lint.fnl
@@ -33,8 +33,7 @@ the `file.diagnostics` field, filling it with diagnostics."
      {:range (message.ast->range server file symbol)
       :message (.. "unused definition: " (tostring symbol))
       :severity message.severity.WARN
       :code 301
       :codeDescription "unused-definition"}
       :code :unused-definition}
      #[{:range (message.ast->range server file symbol)
         :newText (.. "_" (tostring symbol))}])))

@@ -57,8 +56,7 @@ the `file.diagnostics` field, filling it with diagnostics."
         {:range (message.ast->range server file symbol)
          :message (.. "unknown field: " (tostring symbol))
          :severity message.severity.WARN
          :code 302
          :codeDescription "unknown-module-field"}))))
          :code :unknown-module-field}))))

(λ unknown-module-field [server file]
  "any multisym whose definition can't be found through a (require) call"
@@ -83,8 +81,7 @@ the `file.diagnostics` field, filling it with diagnostics."
         :message (.. "unnecessary : call: use (" (tostring (. call 2))
                      ":" method ")")
         :severity message.severity.WARN
         :code 303
         :codeDescription "unnecessary-method"}))))
         :code :unnecessary-method}))))

(λ unnecessary-tset [server file head call]
  (λ all-syms? [call start end]
@@ -109,8 +106,7 @@ the `file.diagnostics` field, filling it with diagnostics."
      (diagnostic {:range (message.ast->range server file call)
                   :message (.. "unnecessary " (tostring head))
                   :severity message.severity.WARN
                   :code 309
                   :codeDescription "unnecessary-tset"}
                   :code :unnecessary-tset}
                  #[{:range (message.ast->range server file call)
                     :newText (make-new-text call)}])))

@@ -120,8 +116,7 @@ the `file.diagnostics` field, filling it with diagnostics."
      (diagnostic {:range (message.ast->range server file call)
                   :message (.. "unnecessary " (tostring head))
                   :severity message.severity.WARN
                   :code 310
                   :codeDescription "unnecessary-do-values"}
                   :code :unnecessary-do-values}
                  #[{:range (message.ast->range server file call)
                     :newText (view (. call 2))}])))

@@ -135,8 +130,7 @@ the `file.diagnostics` field, filling it with diagnostics."
        (diagnostic {:range (message.ast->range server file last-body)
                     :message "redundant do"
                     :severity message.severity.WARN
                     :code 311
                     :codeDescription "redundant-do"}
                     :code :redundant-do}
                    #[{:range (message.ast->range server file last-body)
                       :newText (table.concat
                                 (fcollect [i 2 (length last-body)]
@@ -165,8 +159,7 @@ the `file.diagnostics` field, filling it with diagnostics."
                        (.. " Use a loop when you have a dynamic number of "
                            "arguments to (" (tostring op) ")")))
         :severity message.severity.WARN
         :code 304
         :codeDescription "bad-unpack"}
         :code :bad-unpack}
        (if (and (= (length call) 2)
                 (= (length (. call 2)) 2)
                 (sym? op ".."))
@@ -181,8 +174,7 @@ the `file.diagnostics` field, filling it with diagnostics."
                   :message (.. "var is never set: " (tostring symbol)
                                " Consider using (local) instead of (var)")
                   :severity message.severity.WARN
                   :code 305
                   :codeDescription "var-never-set"})))
                   :code :var-never-set})))

(local op-identity-value {:+ 0 :* 1 :and true :or false :band -1 :bor 0 :.. ""})
(λ op-with-no-arguments [server file op call]
@@ -196,8 +188,7 @@ the `file.diagnostics` field, filling it with diagnostics."
        {:range  (message.ast->range server file call)
         :message (.. "write " (view identity) " instead of (" (tostring op) ")")
         :severity message.severity.WARN
         :code 306
         :codeDescription "op-with-no-arguments"}
         :code :op-with-no-arguments}
        #[{:range (message.ast->range server file call)
           :newText (view identity)}]))))

@@ -207,8 +198,7 @@ the `file.diagnostics` field, filling it with diagnostics."
       {:range  (message.ast->range server file call)
        :message "Use increasing operator instead of decreasing"
        :severity message.severity.WARN
        :code 312
        :codeDescription "no-decreasing-comparison"}
        :code :no-decreasing-comparison}
       #[{:range (message.ast->range server file call)
          :newText (let [new (if (sym? op :>=) (fennel.sym :<=) (fennel.sym :<))
                         reversed (fcollect [i (length call) 2 -1
@@ -230,8 +220,7 @@ the `file.diagnostics` field, filling it with diagnostics."
    (diagnostic {:range (message.ast->range server file (. ast 1))
                 :message "no pinned patterns; use case instead of match"
                 :severity message.severity.WARN
                 :code 308
                 :codeDescription "match-should-case"}
                 :code :match-should-case}
                #[{:range (message.ast->range server file (. ast 1))
                   :newText "case"}])))

@@ -250,8 +239,7 @@ the `file.diagnostics` field, filling it with diagnostics."
     :message (.. "bad " (tostring (. arg 1))
                  " call: only the first value of the multival will be used")
     :severity message.severity.WARN
     :code 307
     :codeDescription "bad-unpack"}))
     :code :inline-unpack}))

(λ add-lint-diagnostics [server file]
  "fill up the file.diagnostics table with linting things"
@@ -269,7 +257,7 @@ the `file.diagnostics` field, filling it with diagnostics."
    ;; all non-macro calls. This only covers specials and function calls.
    (each [[head &as call] (pairs file.calls)]
      (when head
        (when lints.bad-unpack
        (when (or lints.bad-unpack lints.inline-unpack)
          (table.insert diagnostics (bad-unpack server file head call)))
        (when lints.unnecessary-method
          (table.insert diagnostics (unnecessary-method server file head call)))
diff --git a/src/fennel-ls/message.fnl b/src/fennel-ls/message.fnl
index a142a6c..8419f95 100644
--- a/src/fennel-ls/message.fnl
+++ b/src/fennel-ls/message.fnl
@@ -82,8 +82,8 @@ LSP json objects."
                :unnecessary-do-values "Remove unnecessary do or values"
                :redundant-do "Remove redundant do"
                :bad-unpack "Replace with table.concat call"}]
    (or (. titles diag.codeDescription)
        (.. "Action title missing - " diag.codeDescription))))
    (or (. titles diag.code)
        (.. "Action title missing - " diag.code))))

(λ diagnostic->code-action [_server file diagnostic ?kind]
  (let [{: uri} file]
diff --git a/test/lint.fnl b/test/lint.fnl
index 1d11eb1..3faed0e 100644
--- a/test/lint.fnl
+++ b/test/lint.fnl
@@ -43,23 +43,24 @@
(fn test-unused []
  (check "(local x 10)"
         [{:message "unused definition: x"
           :code 301
           :code :unused-definition
           :range {:start {:character 7 :line 0}
                   :end   {:character 8 :line 0}}}])
  (check "(fn x [])"
         [{:message "unused definition: x"
           :code 301
           :code :unused-definition
           :range {:start {:character 4 :line 0}
                   :end   {:character 5 :line 0}}}])
  (check "(let [(x y) (values 1 2)] x)"
         [{:code 301
         [{:code :unused-definition
           :range {:start {:character 9  :line 0}
                   :end   {:character 10 :line 0}}}])
  (check "(case [1 1 2 3 5 8] [a a] (print :first-two-equal))" [{:code 301}])
  (check "(case [1 1 2 3 5 8] [a a] (print :first-two-equal))"
         [{:code :unused-definition}])
  (assert-ok "(case [1 1 2 3 5 8] [a_ a_] (print :first-two-equal))")
  ;; setting a var without reading
  (check "(var x 1) (set x 2) (set [x] [3])"
          [{:code 301
          [{:code :unused-definition
            :range {:start {:character 5 :line 0}
                    :end   {:character 6 :line 0}}}])
  ;; setting a field without reading is okay
@@ -93,39 +94,39 @@
          :main.fnl
          "(local {: a : c &as guy} (require :the-guy-they-tell-you-not-to-worry-about))
           (print guy.b guy.d)"}
         [{:code 302 :message "unknown field: c"}
          {:code 302 :message "unknown field: guy.d"}]
         [{:code 302 :message "unknown field: a"}
          {:code 302 :message "unknown field: b"}])
         [{:code :unknown-module-field :message "unknown field: c"}
          {:code :unknown-module-field :message "unknown field: guy.d"}]
         [{:code :unknown-module-field :message "unknown field: a"}
          {:code :unknown-module-field :message "unknown field: b"}])
  (check "table.insert2 table.insert"
         [{:code 302 :message "unknown field: table.insert2"}]
         [{:code 302 :message "unknown field: table.insert"}])
         [{:code :unknown-module-field :message "unknown field: table.insert2"}]
         [{:code :unknown-module-field :message "unknown field: table.insert"}])
  ;; if you explicitly write "_G", it should turn off this test.
  ;; Hardcoded at the top of analyzer.fnl/search-document.
  (check "_G.insert2"
         []
         [{:code 302}])
         [{:code :unknown-module-field}])
  ;; we don't care about nested
  (check {:requireme.fnl "{:field []}"
          :main.fnl "(local {: field} (require :requireme))
                     field.unknown"}
         []
         [{:code 302}])
         [{:code :unknown-module-field}])
  ;; specials are OK too
  (check {:unpacker.fnl "(local unpack (or table.unpack _G.unpack)) {: unpack}"
          :main.fnl "(local u (require :unpacker))
                     (print (u.unpack [:haha :lol]))"}
         []
         [{:code 302}])
         [{:code :unknown-module-field}])
  (check "package.loaded.mymodule io.stderr.write"
         []
         [{:code 302}])
         [{:code :unknown-module-field}])
  nil)

(fn test-unnecessary-colon []
(fn test-unnecessary-method []
  (check "(let [x :haha] (: x :find :a))"
         [{:message "unnecessary : call: use (x:find)"
           :code 303
           :code :unnecessary-method
           :range {:start {:character 15 :line 0}
                   :end   {:character 29 :line 0}}}])

@@ -140,13 +141,13 @@

(fn test-unpack-into-op []
  (check "(+ (unpack [1 2 3]))"
         [{:code 304}])
         [{:code :bad-unpack}])

  (check "(.. (table.unpack [\"hello\" \"world\"]))"
         [{:code 304 :message #($:find "table.concat")}])
         [{:code :bad-unpack :message #($:find "table.concat")}])

  (check "(* (table.unpack [\"hello\" \"world\"]))"
         [{:code 304 :message #(not ($:find "table%.concat"))}])
         [{:code :bad-unpack :message #(not ($:find "table%.concat"))}])

  ;; only when lexical
  (assert-ok "(-> [1 2 3] table.unpack +)")
@@ -154,7 +155,7 @@

(fn test-unset-var []
  (check "(var x nil) (print x)"
         [{:code 305
         [{:code :var-never-set
           :range {:start {:character 5 :line 0}
                   :end   {:character 6 :line 0}}}])

@@ -166,7 +167,7 @@

(fn test-unpack-in-middle []
  (check "(+ 1 2 3 (values 4 5) 6)"
         [{:code 307
         [{:code :inline-unpack
           :range {:start {:line 0 :character 9}
                   :end   {:line 0 :character 21}}}])

@@ -180,14 +181,15 @@
  (assert-ok "(local [tbl key] [{} :k]) (tset tbl key 249)")
  ;; never a good use of tset
  (check "(local tbl {}) (tset tbl :key 9)"
         [{:code 309
           :codeDescription "unnecessary-tset"
         [{:code :unnecessary-tset
           :message "unnecessary tset"
           :range {:start {:character 15 :line 0}
                   :end {:character 32 :line 0}}}])
  (check "(local tbl {}) (tset tbl :key :nested 9)" [{:code 309}])
  (check "(local tbl {}) (tset tbl :key :nested 9)"
         [{:code :unnecessary-tset}])
  ;; Lint only triggers on keys that can be written as a sym
  (check "(local tbl {}) (tset tbl \"hello-world\" 249)" [{:code 309}])
  (check "(local tbl {}) (tset tbl \"hello-world\" 249)"
         [{:code :unnecessary-tset}])
  (assert-ok "(local tbl {}) (tset tbl \"01234567\" 249)")
  (assert-ok "(local tbl {}) (tset tbl \"hello world\" 1)")
  (assert-ok "(local tbl {}) (tset tbl \"0123.4567\" 1)")
@@ -198,14 +200,12 @@
  (assert-ok "(do (print :x) 11)")
  ;; unnecessary do
  (check "(do 9)" [{:message "unnecessary do"
                    :code 310
                    :codeDescription "unnecessary-do-values"
                    :code :unnecessary-do-values
                    :range {:start {:character 0 :line 0}
                            :end {:character 6 :line 0}}}])
  ;; unnecessary values
  (check "(print :hey (values :lol))"
         [{:code 310
           :codeDescription "unnecessary-do-values"
         [{:code :unnecessary-do-values
           :message "unnecessary values"
           :range {:start {:character 12 :line 0}
                   :end {:character 25 :line 0}}}])
@@ -216,8 +216,7 @@
  (assert-ok "(case 134 x (do (print :x x) 11))")
  ;; unnecessary one
  (check "(let [x 29] (do (print 9) x))"
         [{:code 311
           :codeDescription "redundant-do"
         [{:code :redundant-do
           :message "redundant do"
           :range {:start {:character 12 :line 0}
                   :end {:character 28 :line 0}}}])
@@ -242,13 +241,13 @@
  ;; warn: basic no pinning
  (check "(match 91 z (print :yeah2 z))"
         [{:message "no pinned patterns; use case instead of match"
           :code 308
           :code :match-should-case
           :range {:start {:character 1 :line 0}
                   :end {:character 6 :line 0}}}])
  ;; warn: nested no pinning
  (check "(match [32] [lol] (print :nested-no-pin lol))"
         [{:message "no pinned patterns; use case instead of match"
           :code 308
           :code :match-should-case
           :range {:start {:character 1 :line 0}
                   :end {:character 6 :line 0}}}])
  nil)
@@ -268,7 +267,7 @@
{: test-unused
 : test-ampersand
 : test-unknown-module-field
 : test-unnecessary-colon
 : test-unnecessary-method
 : test-unnecessary-tset
 : test-unnecessary-do
 : test-redundant-do
-- 
2.39.5 (Apple Git-154)

[PATCH 3/3] Add instructions to use fennel-ls with Helix.

Details
Message ID
<20250322110509.38672-3-micampe@micampe.it>
In-Reply-To
<20250322110509.38672-1-micampe@micampe.it> (view parent)
Sender timestamp
1742645109
DKIM signature
pass
Download raw message
Patch: +22 -0
---
 docs/installation.md | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/docs/installation.md b/docs/installation.md
index 869db8e..0e197e1 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -93,6 +93,28 @@ If you run into problems, check the [LSP Client Configuration
reference](https://lsp.sublimetext.io/client_configuration/) and double-check
the location of fennel-ls on the $PATH is visible to Sublime Text.

### Helix

Make sure `fennel_ls` is available in your `$PATH` and then add this to your
`~/.config/helix/languages.toml` file:

```toml
[language-server.fennel-ls]
command = "fennel-ls"

[[language]]
name = "fennel"
scope = "source.fennel"
file-types = ["fnl"]
language-servers = ["fennel-ls"]
```

If you have `fnlfmt` you can also add this to the `[[language]]` section:

```toml
formatter = { command = "fnlfmt", args = ["-"]}
```

### Visual Studio Code

You need to [install an extension](https://codeberg.org/adjuvant/vscode-fennel-ls)
-- 
2.39.5 (Apple Git-154)

Re: [PATCH 2/3] Update diagnostics to be spec compliant.

Details
Message ID
<D8MQZIS4NM0G.333T4RUXCM75A@micampe.it>
In-Reply-To
<20250322110509.38672-2-micampe@micampe.it> (view parent)
Sender timestamp
1742645469
DKIM signature
pass
Download raw message
This patch is long but is mostly mechanical subsitution, the only
notable change is that two numeric codes were using the same string id
so I had to make a new one.

> -         :code 304
> -         :codeDescription "bad-unpack"}
> +         :code :bad-unpack}

> -     :code 307
> -     :codeDescription "bad-unpack"}))
> +     :code :inline-unpack}))

> @@ -269,7 +257,7 @@ the `file.diagnostics` field, filling it with diagnostics."
>      ;; all non-macro calls. This only covers specials and function calls.
>      (each [[head &as call] (pairs file.calls)]
>        (when head
> -        (when lints.bad-unpack
> +        (when (or lints.bad-unpack lints.inline-unpack)
>            (table.insert diagnostics (bad-unpack server file head call)))

Re: [PATCH 3/3] Add instructions to use fennel-ls with Helix.

Details
Message ID
<87pli8swvf.fsf@asthra>
In-Reply-To
<20250322110509.38672-3-micampe@micampe.it> (view parent)
Sender timestamp
1742645468
DKIM signature
pass
Download raw message
Michele Campeotto <micampe@micampe.it> writes:

> +Make sure `fennel_ls` is available in your `$PATH` and then add this to your
> +`~/.config/helix/languages.toml` file:

Thanks! I've applied all 3 in this series.

Is it possible to submit a patch to Helix so it can auto-detect this the
way that Emacs does? Would be nice if future versions could make manual
configuration unnecessary.

-Phil

Re: [PATCH 3/3] Add instructions to use fennel-ls with Helix.

Details
Message ID
<D8OA29PGSX7K.34TS92LGYMMTB@micampe.it>
In-Reply-To
<87pli8swvf.fsf@asthra> (view parent)
Sender timestamp
1742800845
DKIM signature
pass
Download raw message
On Sat Mar 22, 2025 at 8:11 PM CET, Phil Hagelberg wrote:
> Is it possible to submit a patch to Helix so it can auto-detect this the
> way that Emacs does? Would be nice if future versions could make manual
> configuration unnecessary.

I thought about it, but I think it should also have syntax highlighting
first. Helix uses tree-sitter so maybe we can reuse the nvim queries, I
can give it a try but I've never looked into tree-sitter much.

Do you think we should wait for a final decision on the fnlm macro path
change and add that too?


michele

Re: [PATCH 3/3] Add instructions to use fennel-ls with Helix.

Details
Message ID
<87ldsusaww.fsf@asthra>
In-Reply-To
<D8OA29PGSX7K.34TS92LGYMMTB@micampe.it> (view parent)
Sender timestamp
1742804933
DKIM signature
pass
Download raw message
"Michele Campeotto" <micampe@micampe.it> writes:

> I thought about it, but I think it should also have syntax highlighting
> first. Helix uses tree-sitter so maybe we can reuse the nvim queries, I
> can give it a try but I've never looked into tree-sitter much.

Ah, well I was thinking about it like it would be a 1-line change. I
mean if you want to go further with it that's up to you. =)

> Do you think we should wait for a final decision on the fnlm macro path
> change and add that too?

There hasn't really been any discussion about the issue I opened, so
I'll take that to mean there's no objections. I think it's a good idea,
and I'll probably go thru with it at this point since nothing's come up.

-Phil
Reply to thread Export thread (mbox)