From a044dec7f94d0d21bd65cf8a04f09cc63324db18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 3 Jun 2020 18:40:58 +0100 Subject: [PATCH] Delegate "hover" and "signature" doc synchronization efforts to Eldoc 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 --- NEWS.md | 11 +++ README.md | 6 -- eglot.el | 259 +++++++++++++++++------------------------------------- 3 files changed, 91 insertions(+), 185 deletions(-) diff --git a/NEWS.md b/NEWS.md index 9be8588d..cb72ba3b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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]) diff --git a/README.md b/README.md index edbc7794..caf03baa 100644 --- a/README.md +++ b/README.md @@ -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; diff --git a/eglot.el b/eglot.el index 733b69c3..b94fcc31 100644 --- a/eglot.el +++ b/eglot.el @@ -7,7 +7,7 @@ ;; Maintainer: João Távora ;; 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 @@ -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))) @@ -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)) @@ -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 @@ -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 @@ -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)) @@ -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)))))))) @@ -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))) @@ -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'." @@ -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)