Skip to content

Commit

Permalink
Delegate "hover" and "signature" doc synchronization efforts to Eldoc
Browse files Browse the repository at this point in the history
Uses Eldoc's eldoc-documentation-functions variable.  In Eldoc v1.0.0
that variable was already available as a way of handling/composing
multiple docstrings from different sources, but it didn't work
practically with mutiple concurrent async sources.  This was fixed in
1.1.0, which Eglot now requires.

This fixes the synchronization problems reported in #494 and also
issue #439.  It is likely that some of the exact doc-composing
functionality in Eglot, (developed during those issues) was lost, and
has to be remade, quite likely in Eldoc itself.

Flymake is now also an Eldoc producer, and therefore the problems of
github issues #481 and #454 will also soon be fixed as soon as Eglot
starts using the upcoming Flymake 1.0.9.

* NEWS.md: New entry.

* README.md (eglot-put-doc-in-help-buffer)
(eglot-auto-display-help-buffer):  Remove mention to these options.

* eglot.el
(Package-Requires:) Require eldoc.el 1.1.0.
(eglot--when-live-buffer): Rename from eglot--with-live-buffer.
(eglot--when-buffer-window): New macro.
(eglot--after-change, eglot--on-shutdown, eglot-ensure): Use eglot--when-live-buffer.
(eglot--managed-mode): Use eglot-documentation-functions and eldoc-documentation-strategy.
(eglot--highlights): Move down.
(eglot-signature-eldoc-function, eglot-hover-eldoc-function)
(eglot--highlight-piggyback):  New eldoc functions.
(eglot--help-buffer, eglot--update-doc)
(eglot-auto-display-help-buffer, eglot-put-doc-in-help-buffer)
(eglot--truncate-string, eglot-doc-too-large-for-echo-area)
(eglot-help-at-point): Remove all of this.
(eglot--apply-workspace-edit): Call eldoc manually after an edit.
(eglot-mode-map): Remap display-local-help to eldoc-doc-buffer
  • Loading branch information
joaotavora committed Jul 9, 2020
1 parent ac9239b commit a044dec
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 185 deletions.
11 changes: 11 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

Thanks to Ingo Lohmar for the original implementation.

##### Handle multiple "documentation at point" sources ([#439][github#439], [#494][github#494], [#481][github#481], [#454][github#454])

Such sources include as LSP's signature, hover and also the Flymake
diagnostic messages. They can all be presented in the echo area
(space permitting), or via `C-h .`. For now, composition of different
sources can be customized using `eldoc-documentation-strategy`,
`eldoc-echo-area-use-multiline-p` and `eldoc-prefer-doc-buffer`.

The variables `eglot-put-doc-in-help-buffer` and
`eglot-auto-display-help-buffer` have been removed.

# 1.6 (16/04/2020)

##### Column offset calculation is now LSP-conform ([#361][github#361])
Expand Down
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,6 @@ documentation on what these do.
- `eglot-ignored-server-capabilites`: LSP server capabilities that
Eglot could use, but won't;

- `eglot-put-doc-in-help-buffer`: If non-nil, put eldoc docstrings in
separate `*eglot-help*` buffer;

- `eglot-auto-display-help-buffer`: If non-nil, automatically display
`*eglot-help*` buffer;

- `eglot-confirm-server-initiated-edits`: If non-nil, ask for confirmation
before allowing server to edit the source buffer's text;

Expand Down
259 changes: 80 additions & 179 deletions eglot.el
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
;; Maintainer: João Távora <joaotavora@gmail.com>
;; URL: https://github.com/joaotavora/eglot
;; Keywords: convenience, languages
;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8") (project "0.3.0") (xref "1.0.1") (eldoc "1.0.0"))
;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8") (project "0.3.0") (xref "1.0.1") (eldoc "1.1.0"))

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -470,11 +470,19 @@ treated as in `eglot-dbind'."

;;; API (WORK-IN-PROGRESS!)
;;;
(cl-defmacro eglot--with-live-buffer (buf &rest body)
(cl-defmacro eglot--when-live-buffer (buf &rest body)
"Check BUF live, then do BODY in it." (declare (indent 1) (debug t))
(let ((b (cl-gensym)))
`(let ((,b ,buf)) (if (buffer-live-p ,b) (with-current-buffer ,b ,@body)))))

(cl-defmacro eglot--when-buffer-window (buf &body body)
"Check BUF showing somewhere, then do BODY in it" (declare (indent 1) (debug t))
(let ((b (cl-gensym)))
`(let ((,b ,buf))
;;notice the exception when testing with `ert'
(when (or (get-buffer-window ,b) (ert-running-test))
(with-current-buffer ,b ,@body)))))

(cl-defmacro eglot--widening (&rest body)
"Save excursion and restriction. Widen. Then run BODY." (declare (debug t))
`(save-excursion (save-restriction (widen) ,@body)))
Expand Down Expand Up @@ -642,7 +650,7 @@ SERVER. ."
(dolist (buffer (eglot--managed-buffers server))
(let (;; Avoid duplicate shutdowns (github#389)
(eglot-autoshutdown nil))
(eglot--with-live-buffer buffer (eglot--managed-mode-off))))
(eglot--when-live-buffer buffer (eglot--managed-mode-off))))
;; Kill any expensive watches
(maphash (lambda (_id watches)
(mapcar #'file-notify-rm-watch watches))
Expand Down Expand Up @@ -806,7 +814,7 @@ INTERACTIVE is t if called interactively."
((maybe-connect
()
(remove-hook 'post-command-hook #'maybe-connect nil)
(eglot--with-live-buffer buffer
(eglot--when-live-buffer buffer
(unless eglot--managed-mode
(apply #'eglot--connect (eglot--guess-contact))))))
(when buffer-file-name
Expand Down Expand Up @@ -1253,7 +1261,7 @@ and just return it. PROMPT shouldn't end with a question mark."
;;;
(defvar eglot-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [remap display-local-help] 'eglot-help-at-point)
(define-key map [remap display-local-help] 'eldoc-doc-buffer)
map))

(defvar-local eglot--current-flymake-report-fn nil
Expand Down Expand Up @@ -1325,7 +1333,11 @@ Use `eglot-managed-p' to determine if current buffer is managed.")
(add-hook 'change-major-mode-hook #'eglot--managed-mode-off nil t)
(add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t)
(add-hook 'pre-command-hook 'eglot--pre-command-hook nil t)
(eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function)
(eglot--setq-saving eldoc-documentation-functions
'(eglot-signature-eldoc-function
eglot-hover-eldoc-function))
(eglot--setq-saving eldoc-documentation-strategy
#'eldoc-documentation-enthusiast)
(eglot--setq-saving xref-prompt-for-identifier nil)
(eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t))
(eglot--setq-saving company-backends '(company-capf))
Expand Down Expand Up @@ -1733,7 +1745,7 @@ Records BEG, END and PRE-CHANGE-LENGTH locally."
(setq eglot--change-idle-timer
(run-with-idle-timer
eglot-send-changes-idle-time
nil (lambda () (eglot--with-live-buffer buf
nil (lambda () (eglot--when-live-buffer buf
(when eglot--managed-mode
(eglot--signal-textDocument/didChange)
(setq eglot--change-idle-timer nil))))))))
Expand Down Expand Up @@ -2189,9 +2201,7 @@ is not active."
(delete-region (- (point) (length proxy)) (point))
(funcall snippet-fn (or insertText label)))))
(eglot--signal-textDocument/didChange)
(eglot-eldoc-function)))))))

(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.")
(eldoc)))))))

(defun eglot--hover-info (contents &optional range)
(let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range)))
Expand Down Expand Up @@ -2256,177 +2266,68 @@ is not active."
(buffer-string))))
when moresigs concat "\n"))

(defvar eglot--help-buffer nil)
(defun eglot-signature-eldoc-function (cb)
"A member of `eldoc-documentation-functions', for signatures."
(when (eglot--server-capable :signatureHelpProvider)
(let ((buf (current-buffer)))
(jsonrpc-async-request
(eglot--current-server-or-lose)
:textDocument/signatureHelp (eglot--TextDocumentPositionParams)
:success-fn
(eglot--lambda ((SignatureHelp)
signatures activeSignature activeParameter)
(eglot--when-buffer-window buf
(funcall cb
(unless (seq-empty-p signatures)
(eglot--sig-info signatures
activeSignature
activeParameter)))))
:deferred :textDocument/signatureHelp))
t))

(defun eglot--help-buffer ()
(or (and (buffer-live-p eglot--help-buffer)
eglot--help-buffer)
(setq eglot--help-buffer (generate-new-buffer "*eglot-help*"))))
(defun eglot-hover-eldoc-function (cb)
"A member of `eldoc-documentation-functions', for hover."
(when (eglot--server-capable :hoverProvider)
(let ((buf (current-buffer)))
(jsonrpc-async-request
(eglot--current-server-or-lose)
:textDocument/hover (eglot--TextDocumentPositionParams)
:success-fn (eglot--lambda ((Hover) contents range)
(eglot--when-buffer-window buf
(let ((info (unless (seq-empty-p contents)
(eglot--hover-info contents range))))
(funcall cb info :buffer t))))
:deferred :textDocument/hover))
(eglot--highlight-piggyback cb)
t))

(defun eglot-help-at-point ()
"Request documentation for the thing at point."
(interactive)
(eglot--dbind ((Hover) contents range)
(jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover
(eglot--TextDocumentPositionParams))
(let ((blurb (and (not (seq-empty-p contents))
(eglot--hover-info contents range)))
(hint (thing-at-point 'symbol)))
(if blurb
(with-current-buffer (eglot--help-buffer)
(with-help-window (current-buffer)
(rename-buffer (format "*eglot-help for %s*" hint))
(with-current-buffer standard-output (insert blurb))
(setq-local nobreak-char-display nil)))
(display-local-help)))))

(cl-defun eglot-doc-too-large-for-echo-area
(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
interpreted like that variable. If non-nil, the return value is
the number of lines available."
(let ((available-lines (cl-typecase height
(float (truncate (* (frame-height) height)))
(integer height)
(t 1))))
(when (> (1+ (cl-count ?\n string)) available-lines)
available-lines)))

(cl-defun eglot--truncate-string (string height &optional (width (frame-width)))
"Return as much from STRING as fits in HEIGHT and WIDTH.
WIDTH, if non-nil, truncates last line to those columns."
(cl-flet ((maybe-trunc
(str) (if width (truncate-string-to-width str width
nil nil "...")
str)))
(cl-loop
repeat height
for i from 1
for break-pos = (cl-position ?\n string)
for (line . rest) = (and break-pos
(cons (substring string 0 break-pos)
(substring string (1+ break-pos))))
concat (cond (line (if (= i height) (maybe-trunc line) (concat line "\n")))
(t (maybe-trunc string)))
while rest do (setq string rest))))

(defcustom eglot-put-doc-in-help-buffer
;; JT@2020-05-21: TODO: this variable should be renamed and the
;; decision somehow be in eldoc.el itself.
#'eglot-doc-too-large-for-echo-area
"If non-nil, put \"hover\" documentation in separate `*eglot-help*' buffer.
If nil, use whatever `eldoc-message-function' decides, honouring
`eldoc-echo-area-use-multiline-p'. If t, use `*eglot-help*'
unconditionally. If a function, it is called with the
documentation string to display and returns a generalized boolean
interpreted as one of the two preceding values."
:type '(choice (const :tag "Never use `*eglot-help*'" nil)
(const :tag "Always use `*eglot-help*'" t)
(function :tag "Ask a function")))

(defcustom eglot-auto-display-help-buffer nil
"If non-nil, automatically display `*eglot-help*' buffer.
Buffer is displayed with `display-buffer', which obeys
`display-buffer-alist' & friends."
:type 'boolean)
(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.")

(defun eglot--update-doc (string hint)
"Put updated documentation STRING where it belongs.
HINT is used to potentially rename EGLOT's help buffer. If
STRING is nil, the echo area cleared of any previous
documentation. Honour `eglot-put-doc-in-help-buffer',
`eglot-auto-display-help-buffer' and
`eldoc-echo-area-use-multiline-p'."
(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)))
(with-current-buffer (eglot--help-buffer)
(let ((inhibit-read-only t)
(name (format "*eglot-help for %s*" hint)))
(unless (string= name (buffer-name))
(rename-buffer (format "*eglot-help for %s*" hint))
(erase-buffer)
(insert string)
(goto-char (point-min)))
(help-mode)))
(if eglot-auto-display-help-buffer
(display-buffer eglot--help-buffer)
(unless (get-buffer-window eglot--help-buffer t)
;; Hand-tweaked to print two lines. Should it print
;; 1? Or honour max-mini-window-height?
(eglot--message
"%s\n(Truncated, %sfull help in buffer %s)"
(eglot--truncate-string string 1 (- (frame-width) 9))
(if-let (key (car (where-is-internal 'eglot-help-at-point)))
(format "use %s to see " (key-description key)) "")
(buffer-name eglot--help-buffer)))))
((eq eldoc-echo-area-use-multiline-p t)
(if-let ((available (eglot-doc-too-large-for-echo-area string)))
(eldoc-message (eglot--truncate-string string available))
(eldoc-message string)))
((eq eldoc-echo-area-use-multiline-p 'truncate-sym-name-if-fit)
(eldoc-message (eglot--truncate-string string 1 nil)))
(t
;; Can't (yet?) honour non-t non-nil values of this var
(eldoc-message (eglot--truncate-string string 1)))))

(defun eglot-eldoc-function ()
"EGLOT's `eldoc-documentation-function' function."
(let* ((buffer (current-buffer))
(server (eglot--current-server-or-lose))
(position-params (eglot--TextDocumentPositionParams))
sig-showing
(thing-at-point (thing-at-point 'symbol)))
(cl-macrolet ((when-buffer-window
(&body body) ; notice the exception when testing with `ert'
`(when (or (get-buffer-window buffer) (ert-running-test))
(with-current-buffer buffer ,@body))))
(when (eglot--server-capable :signatureHelpProvider)
(jsonrpc-async-request
server :textDocument/signatureHelp position-params
:success-fn
(eglot--lambda ((SignatureHelp)
signatures activeSignature activeParameter)
(when-buffer-window
(when (cl-plusp (length signatures))
(setq sig-showing t)
(eglot--update-doc (eglot--sig-info signatures
activeSignature
activeParameter)
thing-at-point))))
:deferred :textDocument/signatureHelp))
(when (eglot--server-capable :hoverProvider)
(jsonrpc-async-request
server :textDocument/hover position-params
:success-fn (eglot--lambda ((Hover) contents range)
(unless sig-showing
(when-buffer-window
(eglot--update-doc (and (not (seq-empty-p contents))
(eglot--hover-info contents
range))
thing-at-point))))
:deferred :textDocument/hover))
(when (eglot--server-capable :documentHighlightProvider)
(jsonrpc-async-request
server :textDocument/documentHighlight position-params
:success-fn
(lambda (highlights)
(mapc #'delete-overlay eglot--highlights)
(setq eglot--highlights
(when-buffer-window
(mapcar
(eglot--lambda ((DocumentHighlight) range)
(pcase-let ((`(,beg . ,end)
(eglot--range-region range)))
(let ((ov (make-overlay beg end)))
(overlay-put ov 'face 'highlight)
(overlay-put ov 'evaporate t)
ov)))
highlights))))
:deferred :textDocument/documentHighlight))))
eldoc-last-message)
(defun eglot--highlight-piggyback (_cb)
"Request and handle `:textDocument/documentHighlight'"
;; FIXME: Obviously, this is just piggy backing on eldoc's calls for
;; convenience, as shown by the fact that we just ignore cb.
(let ((buf (current-buffer)))
(when (eglot--server-capable :documentHighlightProvider)
(jsonrpc-async-request
(eglot--current-server-or-lose)
:textDocument/documentHighlight (eglot--TextDocumentPositionParams)
:success-fn
(lambda (highlights)
(mapc #'delete-overlay eglot--highlights)
(setq eglot--highlights
(eglot--when-buffer-window buf
(mapcar
(eglot--lambda ((DocumentHighlight) range)
(pcase-let ((`(,beg . ,end)
(eglot--range-region range)))
(let ((ov (make-overlay beg end)))
(overlay-put ov 'face 'highlight)
(overlay-put ov 'evaporate t)
ov)))
highlights))))
:deferred :textDocument/documentHighlight)
nil)))

(defun eglot-imenu ()
"EGLOT's `imenu-create-index-function'."
Expand Down Expand Up @@ -2549,7 +2450,7 @@ documentation. Honour `eglot-put-doc-in-help-buffer',
(unwind-protect
(if prepared (eglot--warn "Caution: edits of files %s failed."
(mapcar #'car prepared))
(eglot-eldoc-function)
(eldoc)
(eglot--message "Edit successful!"))))))

(defun eglot-rename (newname)
Expand Down

1 comment on commit a044dec

@muffinmad
Copy link
Collaborator

Choose a reason for hiding this comment

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

Awesome!

Please sign in to comment.