emacs-lsp / lsp-haskell

lsp-mode :heart: haskell
https://emacs-lsp.github.io/lsp-haskell
GNU General Public License v3.0
236 stars 63 forks source link

Document (?) how to change the `eldoc` signature #151

Open slotThe opened 2 years ago

slotThe commented 2 years ago

Currently, when hovering over an identifier, the first "thing" that we can grab is shown in the minibuffer area (as per the default implementation of lsp-clients-extract-signature-on-hover). While this is probably fine for a lot of languages, for Haskell this seems problematic. As soon as there is at least one type class involved, the hover info will just show the instantiation of that type class and not the signature of the function at point:

2022-06-29-110620_636x131_scrot

I find this to be a bit unintuitive. It's true that knowning the specific instantiations of type classes can be very useful at times, but if one wants to know such things, there is always lsp-ui-doc-{show,glance} or lsp-describe-thing-at-point (which also show all of them and not just the first one in what is probably an arbitrary ordering).

It's possible to override lsp-clients-extract-signature-on-hover with a more appropriate implementation, but that knowledge is somewhat buried inside e.g. the rust documentation, referring to this unmerged pr. Since I think Haskell suffers from this problem as much as Rust does, it might be worth adding it to the documentation here (or somewhere else, I'm not super familiar with how documentation is structured in this project). For example, I have the following in my lsp-mode configuration

(defun slot/lsp-get-type-signature (lang str)
  "Get LANGs type signature in STR.
Original implementation from https://github.com/emacs-lsp/lsp-mode/pull/1740."
  (let* ((start (concat "```" lang))
         (groups (--filter (s-equals? start (car it))
                           (-partition-by #'s-blank? (s-lines (s-trim str)))))
         (name-at-point (symbol-name (symbol-at-point)))
         (type-sig-group (car
                          (--filter (s-contains? name-at-point (cadr it))
                                    groups))))
    (->> (or type-sig-group (car groups))
         (-drop 1)                      ; ``` LANG
         (-drop-last 1)                 ; ```
         (-map #'s-trim)
         (s-join " "))))

(cl-defmethod lsp-clients-extract-signature-on-hover 
  (contents (_server-id (eql lsp-haskell)))
  "Display the type signature of the function under point."
  (let* ((sig (slot/lsp-get-type-signature "haskell" (plist-get contents :value))))
    (lsp--render-element (concat "```haskell\n" sig "\n```"))))

which, in the above example, produces the "expected" output of

2022-06-29-120008_635x132_scrot

As noted in https://github.com/emacs-lsp/lsp-mode/pull/1740, while this implementation works (even for Rust, I might add) it's probably too hacky/brittle to include somewhere verbatim, but I think it's worth to perhaps add some documentation somewhere on how to get (imo) sane hover behaviour.

michaelpj commented 2 years ago

Ugh, this is pretty ugly. I wonder if this is something we could plausibly fix in HLS. It seems reasonable to have the doc of the actual function as the "first" bit of the hover, perhaps?

yyoncho commented 2 years ago

@michaelpj in fact there was, but they deprecated it although IMHO it is much more useful. Check MarkedString in the spec. The best from lsp-mode perspective is to have two items one with language = \ and one with markdown.

michaelpj commented 2 years ago

I meant changing HLS, not the spec :D probably we'd rather avoid deprecated stuff even if it is better

TBH, I don't quite understand the heuristic that's being used for which bit goes in the eldoc, could someone give me a one-sentence summary? Then we can maybe make HLS produce output that does the right thing.

slotThe commented 2 years ago

TBH, I don't quite understand the heuristic that's being used for which bit goes in the eldoc, could someone give me a one-sentence summary? Then we can maybe make HLS produce output that does the right thing.

Having the type signature as the actual first bit of the response would get us most of the way there, I think. Currently, a typical response we want to process looks like this:

```haskell
$dFoldable :: Foldable []
```

* * *

```haskell
$dContravariant :: Contravariant f
```

* * *

```haskell
$dApplicative :: Applicative f
```

* * *

```haskell
folding :: forall (f :: * -> *) s a. Foldable f => (s -> f a) -> Fold s a
```

*Defined in ‘Control.Lens.Fold’*
 *(lens-5.0.1)*
* * *

```haskell
_ :: (MultiMap k v -> [(k, v)])
-> ((k, v) -> f (k, v)) -> MultiMap k v -> f (MultiMap k v)
```
* * *

```haskell
_ :: forall (f :: * -> *) s a. Foldable f => (s -> f a) -> Fold s a
```

What we probably want to extract here is

folding :: forall (f :: * -> *) s a. Foldable f => (s -> f a) -> Fold s a

The default implementation (where contents is a response the like above) for lsp-clients-extract-signature-on-hover is

(car (s-lines (s-trim (lsp--render-element contents))))

which essentially just gets the first line (modulo the GFM markup), which would be $dFoldable :: Foldable [] here. If we pushed the type signature to the very front, we still could not quite use this (ime, it's not uncommon for the signature to span over multiple lines), but the override would be trivial: "get everything in the first Haskell GFM block".

What I did above more or less does the following:

  1. Search through all Haskell code blocks for the symbol at point; since this is the thing we want to have a signature for, this seems like a safe assumption.
  2. Take the first one if multiple matches are found—this assumes that e.g. usage examples come after the signature (which I haven't seen violated so far).
  3. In case nothing could be found that matches, just use the first thing inside some GFM block (i.e., more or less fall back to the default implementation).
michaelpj commented 2 years ago

I don't know whether HLS will ever actually return a signature across multiple lines, so reordering it might just be sufficient. It's probably better UX for the hover itself too!

slotThe commented 2 years ago

I don't know where it happens (on the HLS side or the lsp-mode side) but we definitely receive multi-line type signatures in Emacs. For example, the function

iAmTooLong :: String -> String -> String -> String -> String -> String -> String -> String
iAmTooLong = undefined

will generate the response

```haskell
iAmTooLong :: String
-> String
-> String
-> String
-> String
-> String
-> String
-> String
```

*Defined at /home/slot/repos/haskell/sandbox/src/Main.hs:4:1*

By default, only iAmTooLong :: String is shown in the minibuffer.

michaelpj commented 2 years ago

Great example. That seems like something that could plausibly be improved in lsp-mode, some behaviour like "grab the entire contents of the first GFM block if there is one". WDYT @yyoncho ?

michaelpj commented 2 years ago

(For some reason since yesterday Ctrl-Enter now triggers close+comment instead of comment. I'm going insane.)

slotThe commented 9 months ago

Any updates on this? FWIW, I've been using a variation of the code posted in the OP for quite a while now, and it never broke :)

michaelpj commented 9 months ago

I'd accept a PR that changes things, but I'd prefer a generic change in lsp-mode if possible.