Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possibility to show full first line of hover info #459

Closed

Conversation

muffinmad
Copy link
Collaborator

Let's continue our journey to perfect eldoc message! :)

This PR provides the possibility to show a full first line of the hover/signature info.

Current behavior

eldoc-echo-area-use-multiline-p value What part of the hover info we see in the echo area
nil First line truncated to the frame width
"Hey, where is the rest of the signature info? I just finished with the tenth arg of the func and want to know what the eleventh one is about!"
non-nil Last lines
"Why, Eglot, why?"

Proposed behavior

eldoc-echo-area-use-multiline-p value What part of the hover info we see in the echo area
nil First line truncated to the frame width
"The echo area occupy only one line and doesn't break my shiny windows layout. Whoo-hoo!"
t Top lines
"I can see as much hover info as the echo area can fit. Very informative! Awesome!"
non-nil Full first line
"OMG this is the most convenient way to display hover and (especially) signature info! Great job, Eglot!"

Bonus

Honor eglot--message prefix length when truncating message.

(setq eglot-put-doc-in-help-buffer t)

The echo area content:

Current behavior

[eglot] datetime(year: int, month: int, day: int, hour: int=..., minute: int=...
, sec...
(...truncated. Full help is in `*eglot-help for datetime*')

Proposed behavior

[eglot] datetime(year: int, month: int, day: int, hour: int=..., minute: int=...
(...truncated. Full help is in `*eglot-help for datetime*')

Thanks!

@muffinmad
Copy link
Collaborator Author

This can probably fix #117

@joaotavora
Copy link
Owner

:-) nice job 👏 👏 👏

I think the idea is good, but it doens't follow the semantics of the eldoc variable exactly. However, that wasn't happening anyway, and I think I can live with that, especially given such a convincing presentation. We can look at the implementation tomorrow.

joaotavora added a commit to muffinmad/eglot that referenced this pull request May 21, 2020
Do some refactoring to join similar logic in
eglot-doc-too-large-for-echo-area and eglot--truncate-string.

* eglot.el (eglot-doc-too-large-for-echo-area): Now returns the
number of lines available.
(eglot--truncate-string): New helper.
(eglot--first-line-of-doc, eglot--top-lines-of-doc): Remove.
(eglot--update-doc): Use new helpers.
@joaotavora
Copy link
Owner

Hi @muffinmad , I did some adjustments to this PR, to join some duplicated logic, improving efficiency, but hopefully keeping the behaviour you want. I'm in a hurry, so I couldn't even test, but it shouldn't be very far off. Have a look when you can.

@muffinmad
Copy link
Collaborator Author

Alongside with failed newly added test following error occurred.

from datetime import datetime

With point at the end of that line:

Debugger entered--Lisp error: (wrong-type-argument number-or-marker-p nil)
  1-(nil)
  (setq --cl-var-- (1- --cl-var--))
  (>= (setq --cl-var-- (1- --cl-var--)) 0)
  (and (>= (setq --cl-var-- (1- --cl-var--)) 0) (progn (setq break-pos (cl-position 10 string)) (setq rest (and break-pos (cons (substring string 0 break-pos) (substring string (1+ break-pos)))) line (car-safe (prog1 rest (setq rest (cdr rest))))) (if line (progn (setq --cl-var-- (concat --cl-var-- (if (= i 1) "" "\n"))) (setq --cl-var-- (concat --cl-var-- (if (= i height) (truncate-string-to-width line width nil nil "...") line)))) (setq --cl-var-- (concat --cl-var-- string))) rest))
  (while (and (>= (setq --cl-var-- (1- --cl-var--)) 0) (progn (setq break-pos (cl-position 10 string)) (setq rest (and break-pos (cons (substring string 0 break-pos) (substring string (1+ break-pos)))) line (car-safe (prog1 rest (setq rest (cdr rest))))) (if line (progn (setq --cl-var-- (concat --cl-var-- (if ... "" "\n"))) (setq --cl-var-- (concat --cl-var-- (if ... ... line)))) (setq --cl-var-- (concat --cl-var-- string))) rest)) (setq string rest) (setq i (+ i 1)) (setq --cl-var-- nil))
  (let* ((--cl-var-- height) (i 1) (break-pos nil) (rest nil) (line nil) (--cl-var-- "") (--cl-var-- t)) (while (and (>= (setq --cl-var-- (1- --cl-var--)) 0) (progn (setq break-pos (cl-position 10 string)) (setq rest (and break-pos (cons (substring string 0 break-pos) (substring string ...))) line (car-safe (prog1 rest (setq rest ...)))) (if line (progn (setq --cl-var-- (concat --cl-var-- ...)) (setq --cl-var-- (concat --cl-var-- ...))) (setq --cl-var-- (concat --cl-var-- string))) rest)) (setq string rest) (setq i (+ i 1)) (setq --cl-var-- nil)) --cl-var--)
  (progn (if --cl-rest-- (signal 'wrong-number-of-arguments (list 'eglot--truncate-string (+ 3 (length --cl-rest--))))) (let* ((--cl-var-- height) (i 1) (break-pos nil) (rest nil) (line nil) (--cl-var-- "") (--cl-var-- t)) (while (and (>= (setq --cl-var-- (1- --cl-var--)) 0) (progn (setq break-pos (cl-position 10 string)) (setq rest (and break-pos (cons ... ...)) line (car-safe (prog1 rest ...))) (if line (progn (setq --cl-var-- ...) (setq --cl-var-- ...)) (setq --cl-var-- (concat --cl-var-- string))) rest)) (setq string rest) (setq i (+ i 1)) (setq --cl-var-- nil)) --cl-var--))
  (let* ((width (if --cl-rest-- (car-safe (prog1 --cl-rest-- (setq --cl-rest-- (cdr --cl-rest--)))) (frame-width)))) (progn (if --cl-rest-- (signal 'wrong-number-of-arguments (list 'eglot--truncate-string (+ 3 (length --cl-rest--))))) (let* ((--cl-var-- height) (i 1) (break-pos nil) (rest nil) (line nil) (--cl-var-- "") (--cl-var-- t)) (while (and (>= (setq --cl-var-- (1- --cl-var--)) 0) (progn (setq break-pos (cl-position 10 string)) (setq rest (and break-pos ...) line (car-safe ...)) (if line (progn ... ...) (setq --cl-var-- ...)) rest)) (setq string rest) (setq i (+ i 1)) (setq --cl-var-- nil)) --cl-var--)))
  eglot--truncate-string(#("datetime(year: int, month: int, day: int, hour: in..." 15 18 (face font-lock-builtin-face) 27 30 (face font-lock-builtin-face) 37 40 (face font-lock-builtin-face) 48 51 (face font-lock-builtin-face) 65 68 (face font-lock-builtin-face) 82 85 (face font-lock-builtin-face) 104 107 (face font-lock-builtin-face)) nil)
  (eldoc-message (eglot--truncate-string string (eglot-doc-too-large-for-echo-area string)))
  (cond ((null string) (eldoc-message nil)) ((or (eq t eglot-put-doc-in-help-buffer) (and eglot-put-doc-in-help-buffer (funcall eglot-put-doc-in-help-buffer string))) (save-current-buffer (set-buffer (eglot--help-buffer)) (let ((inhibit-read-only t) (name (format "*eglot-help for %s*" hint))) (if (string= name (buffer-name)) nil (rename-buffer (format "*eglot-help for %s*" hint)) (erase-buffer) (insert string) (goto-char (point-min))) (if eglot-auto-display-help-buffer (display-buffer (current-buffer)) (if (get-buffer-window (current-buffer)) nil (eglot--message "%s\n(...truncated. Full help is in `%s')" (eglot--truncate-string string 1 ...) (buffer-name eglot--help-buffer)))) (help-mode)))) ((eq eldoc-echo-area-use-multiline-p t) (eldoc-message (eglot--truncate-string string (eglot-doc-too-large-for-echo-area string)))) (t (eldoc-message (eglot--truncate-string string 1))))
  eglot--update-doc(#("datetime(year: int, month: int, day: int, hour: in..." 15 18 (face font-lock-builtin-face) 27 30 (face font-lock-builtin-face) 37 40 (face font-lock-builtin-face) 48 51 (face font-lock-builtin-face) 65 68 (face font-lock-builtin-face) 82 85 (face font-lock-builtin-face) 104 107 (face font-lock-builtin-face)) #("datetime" 0 8 (fontified t)))
  (save-current-buffer (set-buffer buffer) (eglot--update-doc (and (not (seq-empty-p contents)) (eglot--hover-info contents range)) thing-at-point))
  (progn (save-current-buffer (set-buffer buffer) (eglot--update-doc (and (not (seq-empty-p contents)) (eglot--hover-info contents range)) thing-at-point)))
  (if (or (get-buffer-window buffer) (ert-running-test)) (progn (save-current-buffer (set-buffer buffer) (eglot--update-doc (and (not (seq-empty-p contents)) (eglot--hover-info contents range)) thing-at-point))))
  (if sig-showing nil (if (or (get-buffer-window buffer) (ert-running-test)) (progn (save-current-buffer (set-buffer buffer) (eglot--update-doc (and (not (seq-empty-p contents)) (eglot--hover-info contents range)) thing-at-point)))))
  (progn (eglot--check-object 'Hover object-once (memq 'enforce-required-keys eglot-strict-mode) (memq 'disallow-non-standard-keys eglot-strict-mode) (memq 'check-types eglot-strict-mode)) (if sig-showing nil (if (or (get-buffer-window buffer) (ert-running-test)) (progn (save-current-buffer (set-buffer buffer) (eglot--update-doc (and (not ...) (eglot--hover-info contents range)) thing-at-point))))))
  (let* ((--cl-rest-- object-once) (contents (car (cdr (plist-member --cl-rest-- ':contents)))) (range (car (cdr (plist-member --cl-rest-- ':range))))) (progn (eglot--check-object 'Hover object-once (memq 'enforce-required-keys eglot-strict-mode) (memq 'disallow-non-standard-keys eglot-strict-mode) (memq 'check-types eglot-strict-mode)) (if sig-showing nil (if (or (get-buffer-window buffer) (ert-running-test)) (progn (save-current-buffer (set-buffer buffer) (eglot--update-doc (and ... ...) thing-at-point)))))))
  (let ((object-once jsonrpc-lambda-elem12)) (let* ((--cl-rest-- object-once) (contents (car (cdr (plist-member --cl-rest-- ':contents)))) (range (car (cdr (plist-member --cl-rest-- ':range))))) (progn (eglot--check-object 'Hover object-once (memq 'enforce-required-keys eglot-strict-mode) (memq 'disallow-non-standard-keys eglot-strict-mode) (memq 'check-types eglot-strict-mode)) (if sig-showing nil (if (or (get-buffer-window buffer) (ert-running-test)) (progn (save-current-buffer (set-buffer buffer) (eglot--update-doc ... thing-at-point))))))))
  (closure ((thing-at-point . #("datetime" 0 8 (fontified t))) (sig-showing) (position-params :textDocument (:uri "file:///Users/mad/workspace/test/test.py") :position (:line 0 :character 29)) (server . #<eglot-lsp-server eglot-lsp-server-1fe76b40c8b4>) (buffer . #<buffer test.py>) revert-buffer-preserve-modes eglot--managed-mode company-tooltip-align-annotations company-backends markdown-fontify-code-blocks-natively t) (jsonrpc-lambda-elem12) (let ((object-once jsonrpc-lambda-elem12)) (let* ((--cl-rest-- object-once) (contents (car (cdr ...))) (range (car (cdr ...)))) (progn (eglot--check-object 'Hover object-once (memq 'enforce-required-keys eglot-strict-mode) (memq 'disallow-non-standard-keys eglot-strict-mode) (memq 'check-types eglot-strict-mode)) (if sig-showing nil (if (or ... ...) (progn ...)))))))((:contents [(:value "datetime(year: int, month: int, day: int, hour: in..." :language "python") "datetime(year, month, day[, hour[, minute[, second..."]))
  jsonrpc-connection-receive(#<eglot-lsp-server eglot-lsp-server-1fe76b40c8b4> (:jsonrpc "2.0" :id 6 :result (:contents [(:value "datetime(year: int, month: int, day: int, hour: in..." :language "python") "datetime(year, month, day[, hour[, minute[, second..."])))
  jsonrpc--process-filter(#<process EGLOT (test/python-mode)> "Content-Length: 460\15\nContent-Type: application/vsc...")

@joaotavora
Copy link
Owner

Nice, thanks for the test and the simple recipe. I'll look at this asap

@fbergroth
Copy link
Contributor

fbergroth commented May 22, 2020

There seems to be a bug in eglot--truncate-string if there is no trailing newline:
(eglot--truncate-string "aa\nbb" 2) ;; => "aabb"

edit: perhaps all lines (and not just the last) should truncate width?

@joaotavora
Copy link
Owner

joaotavora commented May 22, 2020 via email

joaotavora added a commit to muffinmad/eglot that referenced this pull request May 23, 2020
Do some refactoring to join similar logic in
eglot-doc-too-large-for-echo-area and eglot--truncate-string.

* eglot.el (eglot-doc-too-large-for-echo-area): Now returns the
number of lines available.
(eglot--truncate-string): New helper.
(eglot--first-line-of-doc, eglot--top-lines-of-doc): Remove.
(eglot--update-doc): Use new helpers.
@joaotavora joaotavora force-pushed the muffinmad/eldoc-singleline branch 2 times, most recently from b8ada54 to b54e91a Compare May 23, 2020 22:52
@joaotavora
Copy link
Owner

Hi @muffinmad and @fbergroth. I fixed both problems, and enhanced the test a little bit.

@muffinmad if this works like you wanted I was thinking of squashing this to a commit and pushing with you as the Co-author. Or the other way around, you the author me co-author. Whichever you prefer.

eglot.el Outdated
(t
(eldoc-message (eglot--first-line-of-doc string)))))
;; Can't (yet?) honour non-t non-nil values if this var
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this read "of this var"?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

eglot.el Outdated
(string &optional (height max-mini-window-height))
"Return non-nil if STRING won't fit in echo area of height HEIGHT.
HEIGHT defaults to `max-mini-window-height' (which see) and is
interpret like that variable. If non-nil, the return value is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "interpreted"

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

joaotavora added a commit to muffinmad/eglot that referenced this pull request May 24, 2020
Do some refactoring to join similar logic in
eglot-doc-too-large-for-echo-area and eglot--truncate-string.

* eglot.el (eglot-doc-too-large-for-echo-area): Now returns the
number of lines available.
(eglot--truncate-string): New helper.
(eglot--first-line-of-doc, eglot--top-lines-of-doc): Remove.
(eglot--update-doc): Use new helpers.
@joaotavora joaotavora force-pushed the muffinmad/eldoc-singleline branch from b54e91a to ef3e023 Compare May 24, 2020 00:32
@muffinmad
Copy link
Collaborator Author

if this works like you wanted

Please take a look at the differences between the original PR.

All tests are done with

(setq eglot-put-doc-in-help-buffer nil)
(modify-frame-parameters nil '((width . 70) (height . 20)))

1. (setq eldoc-echo-area-use-multiline-p nil)

No problem here, the first line of the hover info truncated to fame width is shown.

2. (setq eldoc-echo-area-use-multiline-p 'truncate-sym-name-if-fit)

First line of the hover info truncated to frame width is show. Can we show full first line here?

Screenshot 2020-05-24 at 23 13 37

New PR version is on the left

3. (setq eldoc-echo-area-use-multiline-p t)

First lines of the hover info are hidden.

Screenshot 2020-05-24 at 23 15 36

New PR version is on the left

I was thinking of squashing this to a commit and pushing with you as the Co-author. Or the other way around, you the author me co-author. Whichever you prefer.

I'm totally fine with the first option. It's turned to be your PR now ;-)

@joaotavora
Copy link
Owner

I'm totally fine with the first option. It's turned to be your PR now ;-)

Yes, I'm sorry. Though I didnt' erase your commit (dont' lose it yoruslef).

Anyway I'm not going to push until this does exactly what you want. What matters is the behaviour, and then we make the implementation as tight as possible.

…ain)

Co-authored-by: Andreii Kolomoiets <andreyk.mad@gmail.com>

Also do some refactoring to join similar logic in
eglot-doc-too-large-for-echo-area and eglot--truncate-string.

* eglot.el (eglot-doc-too-large-for-echo-area): Now returns the
number of lines available.
(eglot--truncate-string): New helper.
(eglot--first-line-of-doc, eglot--top-lines-of-doc): Remove.
(eglot--update-doc): Use new helpers.

* eglot-tests.el (hover-multiline-doc-locus): New test
@joaotavora joaotavora force-pushed the muffinmad/eldoc-singleline branch from ef3e023 to 9dc0f3f Compare May 24, 2020 21:12
@joaotavora
Copy link
Owner

OK, this should be fixed (both problems you pointed out in 2 and 3). Have a last look and I'll push tomorrow to close this. And thanks very much!

@muffinmad
Copy link
Collaborator Author

OK, this should be fixed (both problems you pointed out in 2 and 3).

Case number 2 works as expected now, but for 3 the first lines of the hover info are still hidden:

(modify-frame-parameters nil '((width . 70)))
(setq eldoc-echo-area-use-multiline-p t)

Screenshot 2020-05-25 at 09 47 11

@joaotavora
Copy link
Owner

I think this third problem should be something solved in eldoc perhaps by setting message-truncate-lines to a non nil value. I.e. eglot (or other eldoc users) shouldn't be making complex math to handle this case. Perhaps @dgutov or @monnier would like to weigh in. By the way @muffinmad, you can make "pull requests" on eldoc.el by forking https://github.com/emacs-mirror/emacs and doing your changes there. It's easy to convert that to a patch that we can push to core, perhaps after discussing it in the Emacs bug tracker. I suggest you get comfortable with this idea ;-)

@joaotavora
Copy link
Owner

In the meantime, I've pushed this version to master, since it's much nicer than what we had before.

@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

So far we've been leaving that responsibility to eglot-documentation-function (binding message-truncate-lines to t). That doesn't need complex math.

Another thing to try is binding resize-mini-windows to nil, I think.

@joaotavora
Copy link
Owner

joaotavora commented May 25, 2020 via email

@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

Indeed, I tried that, but I think something in eldoc itself will then call
eldoc-message again with the last recorded message, but without the binding.

Ah, hm, my bad. Indeed, you can't set the binding this way.

But looking at ElDoc's sources, have you tried the eldoc-echo-area-use-multiline-p defcustom?

@joaotavora
Copy link
Owner

Gotta love the "they are Legion" comment in eldoc.el 🤣

@monnier
Copy link
Contributor

monnier commented May 25, 2020 via email

@monnier
Copy link
Contributor

monnier commented May 25, 2020 via email

@joaotavora
Copy link
Owner

joaotavora commented May 25, 2020

But that should all be hidden behind eldoc-documentation-callback
(the functions placed on the s shouldn't have to worry about that).

Oh sure, but if we can't give each member of those functions a different
callback function, then it's harder to combine the results sanely. In hindsight
it was bad to make elements eldoc-documentation-functions argless functions.
You could have followed a similar path to flymake-diagnostic-functions for
what is a completely analogous problem. (yes Dmitry, Flymake could also
use futures :-) ). Maybe you still can, this API hasn't really been out for longer
than a few weeks in GNU ELPA, I doubt anyone is using it.

Anyway, the current patch I'm preparing doens't suppport the async case and
eldoc-documentation-compose yet. It could support it in the future, and work
much the same way that flymake successfully mixes sync and async sources.
But that's a "nice to have", IMO, I don't see any demand for it.

@monnier
Copy link
Contributor

monnier commented May 25, 2020 via email

@joaotavora
Copy link
Owner

joaotavora commented May 25, 2020

I don't have anything against this futuristic ambitious future stuff, but I definitely don't have time to discuss it, and this problem needs some fixing in the short term (both here, in SLY, and probably also SLIME). So I'm going to propose something much closer to what flymake-diagnostic-functions already does, which is the patch I attach here (let's see what GitHub makes of it: perhaps I should just M-x report-emacs-bug).

I definitely consider this API as "not yet released".
You mean we'd pass the callback function as an argument?
That's a valid option as well, indeed.

In that case I would simplify my patch more and remove the eldoc-documentation-callback thing, and just pass the callback as an arg.

cat 0001-Better-handle-asynchronously-produced-eldoc-docstrin.patch                                                                                                                                                                                                                               master ⬆ ✭ ◼
From cd6ef1b12ea120773cbe239157f06eca93a932cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= <joaotavora@gmail.com>
Date: Mon, 25 May 2020 16:39:40 +0100
Subject: [PATCH] Better handle asynchronously produced eldoc docstrings

* lisp/emacs-lisp/eldoc.el (eldoc-documentation-functions):
Overhaul docstring.
(eldoc-documentation-compose, eldoc-documentation-default): Handle
non-nil, non-string values of elements of
eldoc-documentation-functions.  Use eldoc--handle-multiline.
(eldoc-print-current-symbol-info): Honour non-nil, non-string
values returned by eldoc-documentation-callback.
(eldoc--handle-multiline): New helper.
(Version): Bump to 1.1.0.
---
 lisp/emacs-lisp/eldoc.el | 73 ++++++++++++++++++++++++++++++----------
 1 file changed, 56 insertions(+), 17 deletions(-)

diff --git a/lisp/emacs-lisp/eldoc.el b/lisp/emacs-lisp/eldoc.el
index ef5dbf8103..288f617822 100644
--- a/lisp/emacs-lisp/eldoc.el
+++ b/lisp/emacs-lisp/eldoc.el
@@ -5,7 +5,7 @@
 ;; Author: Noah Friedman <friedman@splode.com>
 ;; Keywords: extensions
 ;; Created: 1995-10-06
-;; Version: 1.0.0
+;; Version: 1.1.0
 ;; Package-Requires: ((emacs "26.3"))
 
 ;; This is a GNU ELPA :core package.  Avoid functionality that is not
@@ -338,12 +338,26 @@ eldoc-display-message-no-interference-p
 
 

 (defvar eldoc-documentation-functions nil
-  "Hook for functions to call to return doc string.
-Each function should accept no arguments and return a one-line
-string for displaying doc about a function etc. appropriate to
-the context around point.  It should return nil if there's no doc
-appropriate for the context.  Typically doc is returned if point
-is on a function-like name or in its arg list.
+  "Hook of functions that produce doc strings.
+Each hook function should accept no arguments and decide whether
+to display a doc short string about the context around point.  If
+the decision and the doc string can be produced quickly, the hook
+function should immediately return the doc string, or nil if
+there's no doc appropriate for the context.  Otherwise, if its
+computation is expensive or can't be performed directly, the hook
+function should save the value bound to
+`eldoc-documentation-callback', and arrange for that callback
+function to be asynchronously called at a later time, passing it
+either nil or the desired doc string.  The hook function should
+then return a non-nil, non-string value.
+
+A current limitation of the asynchronous case is that it is only
+guaranteed to work correctly if the value of
+`eldoc-documentation-function' (notice the singular) is 
+`eldoc-documentation-default'.
+
+Typically doc is returned if point is on a function-like name or
+in its arg list.
 
 Major modes should modify this hook locally, for example:
   (add-hook \\='eldoc-documentation-functions #\\='foo-mode-eldoc nil t)
@@ -351,14 +365,18 @@ eldoc-documentation-functions
 taken into account if the major mode specific function does not
 return any documentation.")
 
+(defun eldoc--handle-multiline (res)
+  "Helper for handling a bit of `eldoc-echo-area-use-multiline-p'."
+  (if eldoc-echo-area-use-multiline-p res
+    (truncate-string-to-width
+     res (1- (window-width (minibuffer-window))))))
+
 (defun eldoc-documentation-default ()
   "Show first doc string for item at point.
 Default value for `eldoc-documentation-function'."
   (let ((res (run-hook-with-args-until-success 'eldoc-documentation-functions)))
-    (when res
-      (if eldoc-echo-area-use-multiline-p res
-        (truncate-string-to-width
-         res (1- (window-width (minibuffer-window))))))))
+    (cond ((stringp res) (eldoc--handle-multiline res))
+          (t res))))
 
 (defun eldoc-documentation-compose ()
   "Show multiple doc string results at once.
@@ -368,13 +386,11 @@ eldoc-documentation-compose
      'eldoc-documentation-functions
      (lambda (f)
        (let ((str (funcall f)))
-         (when str (push str res))
+         (when (stringp str) (push str res))
          nil)))
     (when res
       (setq res (mapconcat #'identity (nreverse res) ", "))
-      (if eldoc-echo-area-use-multiline-p res
-        (truncate-string-to-width
-         res (1- (window-width (minibuffer-window))))))))
+      (eldoc--handle-multiline res))))
 
 (defcustom eldoc-documentation-function #'eldoc-documentation-default
   "Function to call to return doc string.
@@ -408,6 +424,12 @@ eldoc--supported-p
            ;; there's some eldoc support in the current buffer.
            (local-variable-p 'eldoc-documentation-function))))
 
+;; this variable should be unbound, but that confuses
+;; `describe-symbol' for some reason.
+(defvar eldoc-documentation-callback nil
+  "Dynamically bound.  Accessible to `eldoc-documentation-functions'.
+See that function for details.")
+
 (defun eldoc-print-current-symbol-info ()
   "Print the text produced by `eldoc-documentation-function'."
   ;; This is run from post-command-hook or some idle timer thing,
@@ -417,11 +439,28 @@ eldoc-print-current-symbol-info
         ;; Erase the last message if we won't display a new one.
         (when eldoc-last-message
           (eldoc-message nil))
-      (let ((non-essential t))
+      (let ((non-essential t)
+            (buffer (current-buffer)))
         ;; Only keep looking for the info as long as the user hasn't
         ;; requested our attention.  This also locally disables inhibit-quit.
         (while-no-input
-          (eldoc-message (funcall eldoc-documentation-function)))))))
+          (let*
+              ((waiting-for-callback nil)
+               (eldoc-documentation-callback
+                (lambda (string)
+                  (with-current-buffer buffer
+                    ;; JT@2020-05-25: Currently, we expect one single
+                    ;; docstring from the client, we silently swallow
+                    ;; anything the client unexpectedly gives us,
+                    ;; including updates.  This could change.
+                    (when waiting-for-callback
+                      (eldoc-message (eldoc--handle-multiline string))
+                      (setq waiting-for-callback nil)))))
+               (res
+                (funcall eldoc-documentation-function)))
+            (cond ((stringp res) (eldoc-message res))
+                  (res (setq waiting-for-callback t))
+                  (t (eldoc-message nil)))))))))
 
 ;; If the entire line cannot fit in the echo area, the symbol name may be
 ;; truncated or eliminated entirely from the output to make room for the

@joaotavora
Copy link
Owner

The docstring contained an error: it should read: "is only guaranteed to work if the value is eldoc-documentation-default"

@muffinmad muffinmad deleted the muffinmad/eldoc-singleline branch May 25, 2020 18:52
@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

In hindsight
it was bad to make elements eldoc-documentation-functions argless functions.

I disagree. The "futures" solution will most likely have argless functions. So the ad-hoc solution should use them too, then the API could be extended/swapper very easily.

You could have followed a similar path to flymake-diagnostic-functions for
what is a completely analogous problem. (yes Dmitry, Flymake could also
use futures :-) ).

Naturally. There are other places they could be used in, too.

@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

I think what I like about futures is that it opens up the possibility to
extend the API a bit more easily. E.g. adding the possibility to abort
a future.

Yup. Or add an "errback" as well, in which ElDoc will display/log in the execution error using some of its mechanisms.

@joaotavora
Copy link
Owner

I disagree. The "futures" solution will most likely have argless functions. So the ad-hoc solution should use them too, then the API could be extended/swapper very easily.

Well the future isn't here yet, is it? When it does come, it's a question of telling elements of eldoc-documentation-functions that they can ignore the callback arg and start returning a "future" object instead. You'll want a similar scheme for Flymake backends anyway (which has more users, I think).

Anyway, as much as I am convinced of the soundness of Stefan's proposal on futures (it is a reasonably easy concept, though i suspect a bit niche still), I would consider the possibility that we might be over-engineering it here, because I can't think of enormous advantages of futures to Eldoc specifically:

  1. aborting an eldoc call is mostly useless, you're saving a few moments of string manipulation before handing the string to Eldoc (and even those we're thinking of mostly offloading to Eldoc).

  2. Eldoc doesn't have anything reasonably useful to do with a backend that reports that it tried to get a docstring and couldn't.

Curiously, applying the reasoning to Flymake yields opposite results: Flymake does have a compelling use case for those two things.

  1. Making diagnostics once you get them from the external process is reasonably expensive and O(N) with buffer size and in the middle of a potentially lengthy process filter. Being informed that the operation is going stale is a good thing. Best case: kill process immediately.

  2. Flymake collects error reports from the backends, temporarily disabling them if they misbehave a lot.

@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

Well the future isn't here yet, is it?

I just showed an ad-hoc API that would work very close to it.

When it does come, it's a question of telling elements of eldoc-documentation-functions that they can ignore the callback arg and start returning a "future" object instead.

Why do this ugly thing when we can do it better from the start?

aborting an eldoc call is mostly useless, you're saving a few moments of string manipulation before handing the string to Eldoc (and even those we're thinking of mostly offloading to Eldoc)

It would most of the time abort an HTTP request, not local computation.

@joaotavora
Copy link
Owner

Why do this ugly thing when we can do it better from the start?

It's not ugly at all. Just as ugly as multiple semantics for return values. And if you want to do the future thing now, be my guest. I await your implementation.

I just showed an ad-hoc API that would work very close to it.

Go for it. But why go for an ad-hoc API when...

It would most of the time abort an HTTP request, not local computation.

How do you abort those? I.e. where you would be checking for the abort flag in the future object?

@joaotavora
Copy link
Owner

Two more points:

  1. I can't quite understand your company "future" thing as well as I can understand Stefan's idea, which is in line with what I expect from futures.

  2. You can write an aborted-p for callbacks as well, no futuristic technology needed.

@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

I await your implementation.

OK.

But why go for an ad-hoc API when...

It's about equal to complexity of your patch. But if we can get the futures in sooner rather than later, that'd be better.

How do you abort those? I.e. where you would be checking for the abort flag in the future object?

By calling (future-abort fut). And the code that created the future will make sure that this will do something useful (or not).

I can't quite understand your company "future" thing as well as I can understand Stefan's idea, which is in line with what I expect from futures.

You probably know where to look for its documentation, but OK.

Please wait for the alternative patch.

@joaotavora
Copy link
Owner

By calling (future-abort fut).
And the code that created the future will make sure that this will do something useful (or not).

And exactly when will that code do that in the HTTP request scenario that you presented?

@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

Perhaps Eldoc will do that in pre-command-hook? After all, it wouldn't do for the callback to fire right after the user has just typed their text character? Or did something else invalidating the result.

@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

Or, with while-no-input, it can do that right after if while-no-input returned t.

@joaotavora
Copy link
Owner

Perhaps Eldoc will do that in pre-command-hook? After all, it wouldn't do for the callback to fire right after the user has just typed their text character? Or did something else invalidating the result.

That problem is already solved by run-with-idle-timer, which eldoc.el already uses.

Or, with while-no-input, it can do that right after if while-no-input returned t.

By the time that could be useful the presumed HTTP request is already well on its way and you're waiting for a filter or a sentinel call. In this scenario, the only place where is might make sense to check (future-aborted-p fut) (or (callback-aborted-p callback), for that matter) is in the process filter. But in the Eldoc case, the HTTP response will be extremely short, so you're gaining very little.

It's about equal to complexity of your patch.

I seriously doubt that, BTW. To "get the futures in", i.e. to get a futures library in Emacs, you have to, well, develop a futures library. A good one, presumably. That's a new file, and a new section in manual. Not to mention a few weeks of bikeshedding (at best) with comparisons to existing implementations such as https://github.com/skeeto/emacs-aio (which, by the way, looks very good).

To hold up this needed fix while we wait for that to resolve seems nonsensical (and, I suppose, a bit paradigmatic of how the reddit crowd sees emacs development).

I say: let's develop a good futures library and use it as much as we can. It'll be your new baby. But babies need to gestate, so let's not rush it. In the meantime, let's fix this actual problem that Eglot is facing. The eldoc-documentations-functions API is pretty much "unreleased", as Stefan says, so we can always change it afterwards. At worst we put a big disclaimer there like you put in xref.el.

@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

You set up the callback inside run-with-idle-timer. But when does it fire?

is in the process filter. But in the Eldoc case, the HTTP response will be extremely short, so you're gaining very little

Depending on the implementation, it could be parsing a huge JSON or whatever. And some other implementations could terminate the network connection (if they expect the server on the other side to handle it well), or kill an external process.

@joaotavora
Copy link
Owner

joaotavora commented May 25, 2020

You set up the callback inside run-with-idle-timer. But when does it fire?

I don't understand. As soon as the response arrives, of course. As soon as the idle timer fires, you'll have made the request, there's no stopping it.There's also no point in checking while-no-input there because the definition of idle timer is that input has stopped for a while.

Depending on the implementation, it could be parsing a huge JSON or whatever.

For eldoc? Nah. For diagnostics, maybe, indeed we get very large diagnostic responses in Eglot.

But if you really really think that aborting is a killer feature here, I can add a eldoc-callback-aborted-p to my patch, it's pretty easy to implement. Not sure if Eglot should bother checking it though.

And some other implementations could terminate the network connection (if they expect the server on the other side to handle it well), or kill an external process.

Again, the only chance they have to do so in the process filter or sentinel. So by then you'll have wasted the effort.

@dgutov
Copy link
Collaborator

dgutov commented May 25, 2020

As soon as the response arrives, of course.

And is Emacs idle at that time?

There's also no point in checking while-no-input there because the definition of idle timer is that input has stopped for a while.

Yes, true. Only having done a very cursory look at the code, I imagined it doing a sit-for loop until the response arrives. I can see now this is not the case.

For eldoc? Nah. For diagnostics, maybe, indeed we get very large diagnostic responses in Eglot.

This is not only for Eglot, though.

Not sure if Eglot should bother checking it though.

No idea, really.

Again, the only chance they have to do so in the process filter or sentinel.

Not sure why you think so.

Anyway, check out https://debbugs.gnu.org/41531#8.

@joaotavora
Copy link
Owner

And is Emacs idle at that time?

Doesn't matter, by the time you can check if input is pending, you'll have bothered the server and Emacs will have slurped a good chunk of its response into memory.

This is not only for Eglot, though.

Yes, but what "function signature at point" server produces "huge JSON" then?

Again, the only chance they have to do so in the process filter or sentinel. So by then you'll have wasted the effort.
Not sure why you think so.

Because that's the way Emacs works! The only effort you can save is heavy lisp processing in the process filter. And what I'm telling you is that the effort is reasonably small in the eldoc case.

Anyway, check out https://debbugs.gnu.org/41531#8.

Thanks. Doesn't look bad. For a poor man's async primitive, I prefer my version, which matches what flymake.el and url.el (and possibly also websocket.el) do. But of course I can live with your patch, so I'll let Stefan or someone else decide . Let's continue in the Emacs bug tracker.

@dgutov
Copy link
Collaborator

dgutov commented May 26, 2020

Doesn't matter, by the time you can check if input is pending, you'll have bothered the server and Emacs will have slurped a good chunk of its response into memory.

OK, another question: if the result still valid?

Yes, but what "function signature at point" server produces "huge JSON" then?

No idea, a hypothetical one. It's an open API, not everybody is or will be using LSP until the end of time. And it doesn't really have to be huge. A saving is a saving.

The only effort you can save is heavy lisp processing in the process filter.

You can certainly kill the external process outside of it. Saving on CPU expenses in general.

For a poor man's async primitive, I prefer my version

So even the code savings didn't convince you? Both in eldoc.el, and likely in doc functions as well.

which matches what flymake.el and url.el (and possibly also websocket.el) do

Using a global var is so Emacs 23.

@joaotavora
Copy link
Owner

Continued the rest of the conversation in the emacs bug tracker link you posted.

Using a global var is so Emacs 23.

eldoc--callback isn't a global var, it's a special var. Cause Lisp is /special/ :sparkles:

bhankas pushed a commit to bhankas/emacs that referenced this pull request Sep 18, 2022
…oc (again)

Co-authored-by: Andreii Kolomoiets <andreyk.mad@gmail.com>

Also do some refactoring to join similar logic in
eglot-doc-too-large-for-echo-area and eglot--truncate-string.

* eglot.el (eglot-doc-too-large-for-echo-area): Now returns the
number of lines available.
(eglot--truncate-string): New helper.
(eglot--first-line-of-doc, eglot--top-lines-of-doc): Remove.
(eglot--update-doc): Use new helpers.

* eglot-tests.el (hover-multiline-doc-locus): New test
bhankas pushed a commit to bhankas/emacs that referenced this pull request Sep 19, 2022
…oc (again)

Co-authored-by: Andreii Kolomoiets <andreyk.mad@gmail.com>

Also do some refactoring to join similar logic in
eglot-doc-too-large-for-echo-area and eglot--truncate-string.

* eglot.el (eglot-doc-too-large-for-echo-area): Now returns the
number of lines available.
(eglot--truncate-string): New helper.
(eglot--first-line-of-doc, eglot--top-lines-of-doc): Remove.
(eglot--update-doc): Use new helpers.

* eglot-tests.el (hover-multiline-doc-locus): New test
bhankas pushed a commit to bhankas/emacs that referenced this pull request Sep 19, 2022
Co-authored-by: Andreii Kolomoiets <andreyk.mad@gmail.com>

Also do some refactoring to join similar logic in
eglot-doc-too-large-for-echo-area and eglot--truncate-string.

* eglot.el (eglot-doc-too-large-for-echo-area): Now returns the
number of lines available.
(eglot--truncate-string): New helper.
(eglot--first-line-of-doc, eglot--top-lines-of-doc): Remove.
(eglot--update-doc): Use new helpers.

* eglot-tests.el (hover-multiline-doc-locus): New test

#459: joaotavora/eglot#459
jollaitbot pushed a commit to sailfishos-mirror/emacs that referenced this pull request Oct 12, 2022
Co-authored-by: Andreii Kolomoiets <andreyk.mad@gmail.com>

Also do some refactoring to join similar logic in
eglot-doc-too-large-for-echo-area and eglot--truncate-string.

* eglot.el (eglot-doc-too-large-for-echo-area): Now returns the
number of lines available.
(eglot--truncate-string): New helper.
(eglot--first-line-of-doc, eglot--top-lines-of-doc): Remove.
(eglot--update-doc): Use new helpers.

* eglot-tests.el (hover-multiline-doc-locus): New test

GitHub-reference: close joaotavora/eglot#459
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants