From 1e5b753bf46a4eb4fb32a062d6162063303f6cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 02:41:17 +0100 Subject: [PATCH 001/771] Initial commit --- lisp/progmodes/eglot.el | 393 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 lisp/progmodes/eglot.el diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el new file mode 100644 index 00000000000..f667eca867d --- /dev/null +++ b/lisp/progmodes/eglot.el @@ -0,0 +1,393 @@ +;;; eglot.el --- A client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- + +;; Copyright (C) 2017 João Távora + +;; Author: João Távora +;; Keywords: extensions + +;; 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 +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'json) +(require 'cl-lib) +(require 'project) + +(defgroup eglot nil + "Interaction with Language Server Protocol servers" + :prefix "eglot-" + :group 'applications) + +(defvar eglot-executables '((rust-mode . ("rls"))) + "Alist mapping major modes to server executables") + +;;; TODO: Soon to be per-project +(defvar eglot--processes-by-project (make-hash-table :test #'equal)) + +(defun eglot--current-process () + "The current logical EGLOT process" + (let ((cur (project-current))) + (unless cur + (eglot--error "No current project, so no process")) + (gethash cur eglot--processes-by-project))) + +(defmacro eglot--define-process-var (var-sym initval &optional doc) + (declare (indent 2)) + `(progn + (put ',var-sym 'function-documentation ,doc) + (defun ,var-sym (&optional process) + (let* ((proc (or process (eglot--current-process))) + (probe (process-get proc ',var-sym))) + (or probe + (let ((def ,initval)) + (process-put proc ',var-sym def) + def)))) + (gv-define-setter ,var-sym (to-store &optional process) + (let ((prop ',var-sym)) + `(let ((proc (or ,process (eglot--current-process)))) + (process-put proc ',prop ,to-store)))))) + +(eglot--define-process-var eglot--message-mark nil + "Point where next unread message starts") + +(eglot--define-process-var eglot--expected-bytes nil + "How many bytes declared by server") + +(eglot--define-process-var eglot--continuations (make-hash-table) + "A hash table of request ID to continuation lambdas") + +(eglot--define-process-var eglot--events-buffer nil + "A buffer pretty-printing the EGLOT RPC events") + +(cl-defmacro eglot--request (process + method + params + success-fn + &key + error-fn + timeout-fn + (async-p t)) + (append `(eglot--call-with-request + ,process + ,async-p + ,method + ,params + (cl-function ,success-fn)) + (and error-fn + `((cl-function ,error-fn))) + (and timeout-fn + `((cl-function ,timeout-fn))))) + +(defun eglot--command () + (cdr (assoc major-mode eglot-executables))) + +(defun eglot-new-process (&optional interactive) + "Starts a new EGLOT process and initializes it" + (interactive (list t)) + (let ((project (project-current)) + (command (eglot--command))) + (unless command (eglot--error "Cannot work without an LSP executable")) + (unless project (eglot--error "Cannot work without a current project!")) + (let ((current-process (eglot--current-process))) + (when (and current-process + (process-live-p current-process)) + (eglot-quit-server current-process 'sync))) + (let ((good-name + (format "EGLOT server (%s)" + (file-name-base + (directory-file-name + (car (project-roots (project-current)))))))) + (with-current-buffer (get-buffer-create + (format "*%s inferior*" good-name)) + (let* ((proc + (make-process :name good-name + :buffer (current-buffer) + :command command + :connection-type 'pipe + :filter 'eglot--process-filter + :sentinel 'eglot--process-sentinel + :stderr (get-buffer-create (format "*%s stderr*" + good-name)))) + (inhibit-read-only t)) + (puthash (project-current) proc eglot--processes-by-project) + (erase-buffer) + (let ((marker (point-marker))) + (set-marker-insertion-type marker nil) + (setf (eglot--message-mark proc) marker)) + (read-only-mode t) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (insert + (format "\n-----------------------------------\n")))) + (eglot--protocol-initialize proc) + (when interactive + (display-buffer (eglot-events-buffer proc)))))))) + +(defun eglot-quit-server (process &optional sync) + (interactive (list (eglot--current-process))) + (eglot--message "Asking server to terminate") + (eglot--request + process + :shutdown + nil + (lambda (&rest _anything) + (eglot--message "Now asking server to exit") + (process-put process 'eglot--moribund t) + (eglot--process-send process + `(:jsonrpc "2.0" + :method :exit))) + :async-p (not sync) + :timeout-fn (lambda () + (eglot--warn "Brutally deleting existing process %s" + process) + (process-put process 'eglot--moribund t) + (delete-process process)))) + +(defun eglot--process-sentinel (process change) + (with-current-buffer (process-buffer process) + (eglot--debug "Process state changed to %s" change) + (when (not (process-live-p process)) + (cond ((process-get process 'eglot--moribund) + (eglot--message "Process exited with status %s" + (process-exit-status process))) + (t + (eglot--warn "Process unexpectedly changed to %s" change)))))) + +(defun eglot--process-filter (proc string) + (when (buffer-live-p (process-buffer proc)) + (with-current-buffer (process-buffer proc) + (let ((moving (= (point) (process-mark proc))) + (inhibit-read-only t) + (pre-insertion-mark (copy-marker (process-mark proc))) + (expected-bytes (eglot--expected-bytes proc)) + (message-mark (eglot--message-mark proc))) + (save-excursion + ;; Insert the text, advancing the process marker. + (goto-char (process-mark proc)) + (insert string) + (set-marker (process-mark proc) (point))) + (if moving (goto-char (process-mark proc))) + + ;; check for new message header + ;; + (save-excursion + (goto-char pre-insertion-mark) + (let* ((match (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" + (+ (point) 100) + t)) + (new-expected-bytes (and match + (string-to-number (match-string 1))))) + (when new-expected-bytes + (when expected-bytes + (eglot--warn + (concat "Unexpectedly starting new message but %s bytes" + "reportedly remaining from previous one") + expected-bytes)) + (set-marker message-mark (point)) + (setf (eglot--expected-bytes proc) new-expected-bytes) + (setq expected-bytes new-expected-bytes)))) + + ;; check for message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes message-mark)))) + (cond ((not expected-bytes) + (eglot--warn + "Skipping %s bytes of unexpected garbage from process %s" + available-bytes + proc) + (set-marker message-mark (process-mark proc))) + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes message-mark) + expected-bytes)))) + (save-excursion + (save-restriction + (goto-char message-mark) + (narrow-to-region message-mark + message-end) + (eglot--process-receive proc (let ((json-object-type 'plist)) + (json-read))))) + (set-marker message-mark message-end) + (setf (eglot--expected-bytes proc) nil))) + (t + ;; just adding some stuff to the end that doesn't yet + ;; complete the message + ))))))) + +(defun eglot-events-buffer (process &optional interactive) + (interactive (list (eglot--current-process) t)) + (let* ((probe (eglot--events-buffer process)) + (buffer (or (and (buffer-live-p probe) + probe) + (let ((buffer (get-buffer-create + (format "*%s events*" + (process-name process))))) + (with-current-buffer buffer + (buffer-disable-undo) + (read-only-mode t) + (setf (eglot--events-buffer process) + buffer)) + buffer)))) + (when interactive + (pop-to-buffer buffer)) + buffer)) + +(defun eglot--log-event (proc type message) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (goto-char (point-max)) + (insert (format "%s: \n%s\n" type (pp-to-string message)))))) + +(defun eglot--process-receive (proc message) + (let ((inhibit-read-only t)) + (insert (format "Server said:\n%s\n" message))) + (eglot--log-event proc 'server message) + ;; Maybe this is a responsee + ;; + (let* ((response-id (plist-get message :id)) + (err (plist-get message :error)) + (continuations (and response-id + (gethash response-id (eglot--continuations))))) + (cond ((and response-id + (not continuations)) + (eglot--warn "Ooops no continuation for id %s" response-id)) + (continuations + (cancel-timer (third continuations)) + (cond (err + (apply (second continuations) err)) + (t + (apply (first continuations) (plist-get message :result))))) + (t + (eglot--debug "No implemetation for notification %s yet" + (plist-get message :method)))))) + +;; (setq json-encoding-pretty-print nil) ; for debug +(defvar eglot--expect-carriage-return nil) + +(defun eglot--process-send (proc message) + (let* ((json (json-encode message)) + (to-send (format "Content-Length: %d\r\n\r\n%s" + (string-bytes json) + json))) + (process-send-string proc to-send) + (eglot--log-event proc 'client message))) + +(defvar eglot--next-request-id 0) + +(defun eglot--next-request-id () + (setq eglot--next-request-id (1+ eglot--next-request-id))) + +(defun eglot--call-with-request (process + async-p + method + params + success-fn + &optional error-fn timeout-fn) + (let* ((id (eglot--next-request-id)) + (timeout-fn (or timeout-fn + (lambda () + (eglot--warn "Tired of waiting for reply to %s" id) + (remhash id (eglot--continuations process))))) + (error-fn (or error-fn + (cl-function + (lambda (&key code message) + (eglot--warn "Request id=%s errored with code=%s: %s" + id code message))))) + (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) + (eglot--process-send process + `(:jsonrpc "2.0" + :id ,id + :method ,method + :params ,params)) + (catch catch-tag + (puthash id + (list (if async-p + success-fn + (lambda (&rest args) + (throw catch-tag (apply success-fn args)))) + (if async-p + error-fn + (lambda (&rest args) + (throw catch-tag (apply error-fn args)))) + (run-with-timer 5 nil + (if async-p + timeout-fn + (lambda () + (throw catch-tag (apply timeout-fn)))))) + (eglot--continuations process)) + (unless async-p + (while t + (unless (eq (process-status process) 'open) + (eglot--error "Process %s died unexpectedly" process)) + (accept-process-output nil 0.01)))))) + + +(defun eglot--protocol-initialize (process) + (eglot--request + process + :initialize + `(:processId ,(emacs-pid) + :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) + :textDocument (:synchronization (:didSave t))) + ) + (lambda (&key capabilities) + (cl-destructuring-bind + (&rest all + &key + _textDocumentSync + _hoverProvider + _completionProvider + _definitionProvider + _referencesProvider + _documentHighlightProvider + _documentSymbolProvider + _workspaceSymbolProvider + _codeActionProvider + _documentFormattingProvider + _documentRangeFormattingProvider + _renameProvider + _executeCommandProvider + ) + capabilities + (message "so yeah I got lots (%d) of capabilities" (length all)))))) + +(defun eglot--debug (format &rest args) + (display-warning 'eglot + (apply #'format format args) + :debug)) + +(defun eglot--error (format &rest args) + (error (apply #'format format args))) + +(defun eglot--message (format &rest args) + (message (concat "[eglot] " (apply #'format format args)))) + +(defun eglot--warn (format &rest args) + (display-warning 'eglot + (apply #'format format args) + :warning)) + +(provide 'eglot) +;;; eglot.el ends here From 6beef2a347bb9f635194ae765d2e06b1c2c50b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 09:24:46 +0100 Subject: [PATCH 002/771] Remove a couple of comments --- lisp/progmodes/eglot.el | 2 -- 1 file changed, 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f667eca867d..f2ef4184d64 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -36,7 +36,6 @@ (defvar eglot-executables '((rust-mode . ("rls"))) "Alist mapping major modes to server executables") -;;; TODO: Soon to be per-project (defvar eglot--processes-by-project (make-hash-table :test #'equal)) (defun eglot--current-process () @@ -279,7 +278,6 @@ (eglot--debug "No implemetation for notification %s yet" (plist-get message :method)))))) -;; (setq json-encoding-pretty-print nil) ; for debug (defvar eglot--expect-carriage-return nil) (defun eglot--process-send (proc message) From 9f98c5a20d36bb20b2803b6a2ca749f4ad6e66c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 11:36:44 +0100 Subject: [PATCH 003/771] Rename eglot--continuations eglot--pending-continuations --- lisp/progmodes/eglot.el | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f2ef4184d64..166f23ccd8c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -67,7 +67,7 @@ (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") -(eglot--define-process-var eglot--continuations (make-hash-table) +(eglot--define-process-var eglot--pending-continuations (make-hash-table) "A hash table of request ID to continuation lambdas") (eglot--define-process-var eglot--events-buffer nil @@ -264,12 +264,14 @@ (let* ((response-id (plist-get message :id)) (err (plist-get message :error)) (continuations (and response-id - (gethash response-id (eglot--continuations))))) + (gethash response-id (eglot--pending-continuations))))) (cond ((and response-id (not continuations)) (eglot--warn "Ooops no continuation for id %s" response-id)) (continuations (cancel-timer (third continuations)) + (remhash response-id + (eglot--pending-continuations)) (cond (err (apply (second continuations) err)) (t @@ -303,7 +305,7 @@ (timeout-fn (or timeout-fn (lambda () (eglot--warn "Tired of waiting for reply to %s" id) - (remhash id (eglot--continuations process))))) + (remhash id (eglot--pending-continuations process))))) (error-fn (or error-fn (cl-function (lambda (&key code message) @@ -330,7 +332,7 @@ timeout-fn (lambda () (throw catch-tag (apply timeout-fn)))))) - (eglot--continuations process)) + (eglot--pending-continuations process)) (unless async-p (while t (unless (eq (process-status process) 'open) From d663b9282ddd49d7ad880a6fad6b2e07d16f59a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 12:10:13 +0100 Subject: [PATCH 004/771] Add a mode-line construct and some minor fanciness --- lisp/progmodes/eglot.el | 126 +++++++++++++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 166f23ccd8c..92c12162f92 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -64,6 +64,9 @@ (eglot--define-process-var eglot--message-mark nil "Point where next unread message starts") +(eglot--define-process-var eglot--short-name nil + "A short name") + (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") @@ -92,25 +95,29 @@ (and timeout-fn `((cl-function ,timeout-fn))))) -(defun eglot--command () - (cdr (assoc major-mode eglot-executables))) +(defun eglot--command (&optional errorp) + (let ((probe (cdr (assoc major-mode eglot-executables)))) + (unless (or (not errorp) + probe) + (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)) + probe)) -(defun eglot-new-process (&optional interactive) +(defun eglot-new-process (&optional _interactive) "Starts a new EGLOT process and initializes it" (interactive (list t)) (let ((project (project-current)) - (command (eglot--command))) - (unless command (eglot--error "Cannot work without an LSP executable")) + (command (eglot--command 'errorp))) (unless project (eglot--error "Cannot work without a current project!")) (let ((current-process (eglot--current-process))) (when (and current-process (process-live-p current-process)) (eglot-quit-server current-process 'sync))) - (let ((good-name - (format "EGLOT server (%s)" - (file-name-base - (directory-file-name - (car (project-roots (project-current)))))))) + (let* ((short-name (file-name-base + (directory-file-name + (car (project-roots (project-current)))))) + (good-name + (format "EGLOT server (%s)" short-name))) (with-current-buffer (get-buffer-create (format "*%s inferior*" good-name)) (let* ((proc @@ -123,6 +130,7 @@ :stderr (get-buffer-create (format "*%s stderr*" good-name)))) (inhibit-read-only t)) + (setf (eglot--short-name proc) short-name) (puthash (project-current) proc eglot--processes-by-project) (erase-buffer) (let ((marker (point-marker))) @@ -133,9 +141,7 @@ (let ((inhibit-read-only t)) (insert (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize proc) - (when interactive - (display-buffer (eglot-events-buffer proc)))))))) + (eglot--protocol-initialize proc)))))) (defun eglot-quit-server (process &optional sync) (interactive (list (eglot--current-process))) @@ -246,7 +252,7 @@ buffer)) buffer)))) (when interactive - (pop-to-buffer buffer)) + (display-buffer buffer)) buffer)) (defun eglot--log-event (proc type message) @@ -295,6 +301,10 @@ (defun eglot--next-request-id () (setq eglot--next-request-id (1+ eglot--next-request-id))) +(defun eglot-forget-pending-continuations (process) + (interactive (eglot--current-process)) + (clrhash (eglot--pending-continuations process))) + (defun eglot--call-with-request (process async-p method @@ -389,5 +399,93 @@ (apply #'format format args) :warning)) + + +;;; Mode line +;;; + + +(defface eglot-mode-line + '((t (:inherit font-lock-constant-face :weight bold))) + "Face for package-name in EGLOT's mode line." + :group 'eglot) + +(define-minor-mode eglot-mode + "Minor mode for buffers where EGLOT is possible") + +(defvar eglot-menu) + +(defvar eglot-mode-map (make-sparse-keymap)) + +(easy-menu-define eglot-menu eglot-mode-map "SLY" + `("EGLOT" )) + +(defvar eglot--mode-line-format + `(:eval (eglot--mode-line-format))) + +(put 'eglot--mode-line-format 'risky-local-variable t) + +(defun eglot--mode-line-format () + (let* ((proc (eglot--current-process)) + (name (and proc + (process-live-p proc) + (eglot--short-name proc))) + (pending (and proc + (hash-table-count + (eglot--pending-continuations proc)))) + (format-number (lambda (n) (cond ((and n (not (zerop n))) + (format "%d" n)) + (n "-") + (t "*"))))) + (append + `((:propertize "eglot" + face eglot-mode-line + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line down-mouse-1] + eglot-menu) + map) + mouse-face mode-line-highlight + help-echo "mouse-1: pop-up EGLOT menu" + )) + (if name + `(" " + (:propertize + ,name + face eglot-mode-line + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] 'eglot-events-buffer) + (define-key map [mode-line mouse-2] 'eglot-quit-server) + (define-key map [mode-line mouse-3] 'eglot-new-process) + map) + mouse-face mode-line-highlight + help-echo ,(concat "mouse-1: events buffer\n" + "mouse-2: quit server\n" + "mouse-3: new process")) + "/" + (:propertize + ,(funcall format-number pending) + help-echo ,(if name + (format + "%s pending events outgoing\n%s" + pending + (concat "mouse-1: go to events buffer" + "mouse-3: forget pending continuations")) + "No current connection") + mouse-face mode-line-highlight + face ,(cond ((and pending (cl-plusp pending)) + 'warning) + (t + 'eglot-mode-line)) + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] + 'eglot-events-buffer) + (define-key map [mode-line mouse-3] + 'eglot-forget-pending-continuations) + map))))))) + +(add-to-list 'mode-line-misc-info + `(t + (" [" eglot--mode-line-format "] "))) + (provide 'eglot) ;;; eglot.el ends here From 8baf2c7ac20e7d8327cfec5960a788e47d02be67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 12:13:52 +0100 Subject: [PATCH 005/771] Introduce and use `eglot--current-process-or-lose' --- lisp/progmodes/eglot.el | 83 ++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 92c12162f92..5ca6833de71 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -45,12 +45,16 @@ (eglot--error "No current project, so no process")) (gethash cur eglot--processes-by-project))) +(defun eglot--current-process-or-lose () + (or (eglot--current-process) + (eglot--error "No current EGLOT process"))) + (defmacro eglot--define-process-var (var-sym initval &optional doc) (declare (indent 2)) `(progn (put ',var-sym 'function-documentation ,doc) (defun ,var-sym (&optional process) - (let* ((proc (or process (eglot--current-process))) + (let* ((proc (or process (eglot--current-process-or-lose))) (probe (process-get proc ',var-sym))) (or probe (let ((def ,initval)) @@ -58,7 +62,7 @@ def)))) (gv-define-setter ,var-sym (to-store &optional process) (let ((prop ',var-sym)) - `(let ((proc (or ,process (eglot--current-process)))) + `(let ((proc (or ,process (eglot--current-process-or-lose)))) (process-put proc ',prop ,to-store)))))) (eglot--define-process-var eglot--message-mark nil @@ -103,7 +107,7 @@ major-mode)) probe)) -(defun eglot-new-process (&optional _interactive) +(defun eglot-new-process (&optional interactive) "Starts a new EGLOT process and initializes it" (interactive (list t)) (let ((project (project-current)) @@ -141,10 +145,10 @@ (let ((inhibit-read-only t)) (insert (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize proc)))))) + (eglot--protocol-initialize proc interactive)))))) (defun eglot-quit-server (process &optional sync) - (interactive (list (eglot--current-process))) + (interactive (list (eglot--current-process-or-lose))) (eglot--message "Asking server to terminate") (eglot--request process @@ -238,7 +242,7 @@ ))))))) (defun eglot-events-buffer (process &optional interactive) - (interactive (list (eglot--current-process) t)) + (interactive (list (eglot--current-process-or-lose) t)) (let* ((probe (eglot--events-buffer process)) (buffer (or (and (buffer-live-p probe) probe) @@ -302,7 +306,7 @@ (setq eglot--next-request-id (1+ eglot--next-request-id))) (defun eglot-forget-pending-continuations (process) - (interactive (eglot--current-process)) + (interactive (eglot--current-process-or-lose)) (clrhash (eglot--pending-continuations process))) (defun eglot--call-with-request (process @@ -350,38 +354,41 @@ (accept-process-output nil 0.01)))))) -(defun eglot--protocol-initialize (process) +(defun eglot--protocol-initialize (process interactive) (eglot--request - process - :initialize - `(:processId ,(emacs-pid) - :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" - (expand-file-name (car (project-roots - (project-current))))) - :initializationOptions [] - :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) - :textDocument (:synchronization (:didSave t))) - ) - (lambda (&key capabilities) - (cl-destructuring-bind - (&rest all - &key - _textDocumentSync - _hoverProvider - _completionProvider - _definitionProvider - _referencesProvider - _documentHighlightProvider - _documentSymbolProvider - _workspaceSymbolProvider - _codeActionProvider - _documentFormattingProvider - _documentRangeFormattingProvider - _renameProvider - _executeCommandProvider - ) - capabilities - (message "so yeah I got lots (%d) of capabilities" (length all)))))) + process + :initialize + `(:processId ,(emacs-pid) + :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) + :textDocument (:synchronization (:didSave t)))) + (lambda (&key capabilities) + (cl-destructuring-bind + (&rest all + &key + ;; capabilities reported by server + _textDocumentSync + _hoverProvider + _completionProvider + _definitionProvider + _referencesProvider + _documentHighlightProvider + _documentSymbolProvider + _workspaceSymbolProvider + _codeActionProvider + _documentFormattingProvider + _documentRangeFormattingProvider + _renameProvider + _executeCommandProvider + ) + capabilities + (when interactive + (eglot--message + "So yeah I got lots (%d) of capabilities" + (length all))))))) (defun eglot--debug (format &rest args) (display-warning 'eglot From 18b582dde2ef0275561a66fb292823f559ed7a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 12:49:24 +0100 Subject: [PATCH 006/771] Handle notifications --- lisp/progmodes/eglot.el | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5ca6833de71..6e3a1365962 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -287,8 +287,13 @@ (t (apply (first continuations) (plist-get message :result))))) (t - (eglot--debug "No implemetation for notification %s yet" - (plist-get message :method)))))) + (let* ((method (plist-get message :method)) + (handler-sym (intern (concat "eglot--" + method)))) + (if (functionp handler-sym) + (apply handler-sym proc (plist-get message :params)) + (eglot--debug "No implemetation for notification %s yet" + method))))))) (defvar eglot--expect-carriage-return nil) @@ -391,6 +396,14 @@ (length all))))))) (defun eglot--debug (format &rest args) + +;;; Notifications +;;; +(cl-defun eglot--textDocument/publishDiagnostics + (_process &key uri diagnostics) + "Handle notification publishDiagnostics" + (eglot--message "So yeah I got %s for %s" + diagnostics uri)) (display-warning 'eglot (apply #'format format args) :debug)) From a0003aa19a96b350e0468a2f9c43a83d935727db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 12:50:20 +0100 Subject: [PATCH 007/771] Improve `eglot--current-process' --- lisp/progmodes/eglot.el | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6e3a1365962..c7f8774d838 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -41,13 +41,14 @@ (defun eglot--current-process () "The current logical EGLOT process" (let ((cur (project-current))) - (unless cur - (eglot--error "No current project, so no process")) - (gethash cur eglot--processes-by-project))) + (and cur + (gethash cur eglot--processes-by-project)))) (defun eglot--current-process-or-lose () (or (eglot--current-process) - (eglot--error "No current EGLOT process"))) + (eglot--error "No current EGLOT process%s" + (if (project-current) "" + " (Also no current project)")))) (defmacro eglot--define-process-var (var-sym initval &optional doc) (declare (indent 2)) From 18ed39789ac832ba6e1db619930c3e958d8e1bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 12:50:53 +0100 Subject: [PATCH 008/771] Organize a bit --- lisp/progmodes/eglot.el | 50 ++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c7f8774d838..e3b288b0381 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -148,25 +148,6 @@ (format "\n-----------------------------------\n")))) (eglot--protocol-initialize proc interactive)))))) -(defun eglot-quit-server (process &optional sync) - (interactive (list (eglot--current-process-or-lose))) - (eglot--message "Asking server to terminate") - (eglot--request - process - :shutdown - nil - (lambda (&rest _anything) - (eglot--message "Now asking server to exit") - (process-put process 'eglot--moribund t) - (eglot--process-send process - `(:jsonrpc "2.0" - :method :exit))) - :async-p (not sync) - :timeout-fn (lambda () - (eglot--warn "Brutally deleting existing process %s" - process) - (process-put process 'eglot--moribund t) - (delete-process process)))) (defun eglot--process-sentinel (process change) (with-current-buffer (process-buffer process) @@ -359,6 +340,9 @@ (eglot--error "Process %s died unexpectedly" process)) (accept-process-output nil 0.01)))))) + +;;; Requests +;;; (defun eglot--protocol-initialize (process interactive) (eglot--request @@ -366,6 +350,7 @@ :initialize `(:processId ,(emacs-pid) :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" + "file://" (expand-file-name (car (project-roots (project-current))))) :initializationOptions [] @@ -396,7 +381,26 @@ "So yeah I got lots (%d) of capabilities" (length all))))))) -(defun eglot--debug (format &rest args) +(defun eglot-quit-server (process &optional sync) + (interactive (list (eglot--current-process-or-lose))) + (eglot--message "Asking server to terminate") + (eglot--request + process + :shutdown + nil + (lambda (&rest _anything) + (eglot--message "Now asking server to exit") + (process-put process 'eglot--moribund t) + (eglot--process-send process + `(:jsonrpc "2.0" + :method :exit))) + :async-p (not sync) + :timeout-fn (lambda () + (eglot--warn "Brutally deleting existing process %s" + process) + (process-put process 'eglot--moribund t) + (delete-process process)))) + ;;; Notifications ;;; @@ -405,6 +409,12 @@ "Handle notification publishDiagnostics" (eglot--message "So yeah I got %s for %s" diagnostics uri)) + + +;;; Helpers +;;; +(defun + eglot--debug (format &rest args) (display-warning 'eglot (apply #'format format args) :debug)) From b2ea73ca9c80d1f20b4939eacf235dba05fc1aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 12:58:20 +0100 Subject: [PATCH 009/771] Cancel timeouts when process dies unexpectedly --- lisp/progmodes/eglot.el | 53 +++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e3b288b0381..5699775a2bb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -153,9 +153,16 @@ (with-current-buffer (process-buffer process) (eglot--debug "Process state changed to %s" change) (when (not (process-live-p process)) + ;; Remember to cancel all timers + ;; + (maphash (lambda (id v) + (cl-destructuring-bind (_success _error timeout) v + (eglot--message "Cancelling timer for continuation %s" id) + (cancel-timer timeout))) + (eglot--pending-continuations process)) (cond ((process-get process 'eglot--moribund) (eglot--message "Process exited with status %s" - (process-exit-status process))) + (process-exit-status process))) (t (eglot--warn "Process unexpectedly changed to %s" change)))))) @@ -319,26 +326,30 @@ :method ,method :params ,params)) (catch catch-tag - (puthash id - (list (if async-p - success-fn - (lambda (&rest args) - (throw catch-tag (apply success-fn args)))) - (if async-p - error-fn - (lambda (&rest args) - (throw catch-tag (apply error-fn args)))) - (run-with-timer 5 nil - (if async-p - timeout-fn - (lambda () - (throw catch-tag (apply timeout-fn)))))) - (eglot--pending-continuations process)) - (unless async-p - (while t - (unless (eq (process-status process) 'open) - (eglot--error "Process %s died unexpectedly" process)) - (accept-process-output nil 0.01)))))) + (let ((timeout-timer + (run-with-timer 5 nil + (if async-p + timeout-fn + (lambda () + (throw catch-tag (apply timeout-fn))))))) + (puthash id + (list (if async-p + success-fn + (lambda (&rest args) + (throw catch-tag (apply success-fn args)))) + (if async-p + error-fn + (lambda (&rest args) + (throw catch-tag (apply error-fn args)))) + timeout-timer) + (eglot--pending-continuations process)) + (unless async-p + (unwind-protect + (while t + (unless (process-live-p process) + (eglot--error "Process %s died unexpectedly" process)) + (accept-process-output nil 0.01)) + (cancel-timer timeout-timer))))))) ;;; Requests From b864866dac26a959d3f7fabf7eefb7edc03a18ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 13:02:05 +0100 Subject: [PATCH 010/771] Minor cleanup --- lisp/progmodes/eglot.el | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5699775a2bb..479e87ae565 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -117,6 +117,7 @@ (let ((current-process (eglot--current-process))) (when (and current-process (process-live-p current-process)) + (eglot--message "Asking current process to terminate first") (eglot-quit-server current-process 'sync))) (let* ((short-name (file-name-base (directory-file-name @@ -148,7 +149,6 @@ (format "\n-----------------------------------\n")))) (eglot--protocol-initialize proc interactive)))))) - (defun eglot--process-sentinel (process change) (with-current-buffer (process-buffer process) (eglot--debug "Process state changed to %s" change) @@ -353,8 +353,7 @@ ;;; Requests -;;; - +;;; (defun eglot--protocol-initialize (process interactive) (eglot--request process @@ -445,8 +444,6 @@ ;;; Mode line ;;; - - (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) "Face for package-name in EGLOT's mode line." From 584ae9e0b5cfa5f438bd4fa84d191963a33a38bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 13:49:31 +0100 Subject: [PATCH 011/771] Experimental diagnostic overlays --- lisp/progmodes/eglot.el | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 479e87ae565..cd91d7821ce 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -414,11 +414,47 @@ ;;; Notifications ;;; +(defvar-local eglot--diagnostic-overlays nil) + (cl-defun eglot--textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" - (eglot--message "So yeah I got %s for %s" - diagnostics uri)) + (let* ((obj (url-generic-parse-url uri)) + (filename (car (url-path-and-query obj))) + (buffer (find-buffer-visiting filename))) + (cond + (buffer + (with-current-buffer buffer + (eglot--message "OK so add some %s diags" (length diagnostics)) + (mapc #'delete-overlay eglot--diagnostic-overlays) + (setq eglot--diagnostic-overlays nil) + (cl-flet ((pos-at (pos-plist) + (save-excursion + (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char (plist-get pos-plist :character)) + (point)))) + (loop for diag across diagnostics + do (cl-destructuring-bind (&key range severity + _code _source message) + diag + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (pos-at start)) + (end-pos (pos-at end)) + (ov (make-overlay begin-pos + end-pos + buffer))) + (push ov eglot--diagnostic-overlays) + (overlay-put ov 'face + (case severity + (1 'flymake-errline) + (2 'flymake-warnline))) + (overlay-put ov 'help-echo + message) + (overlay-put ov 'eglot--diagnostic diag)))))))) + (t + (eglot--message "OK so %s isn't visited" filename))))) ;;; Helpers From dc6c221a76b3051e0625c40a3bbef2297eec2308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 15:48:50 +0100 Subject: [PATCH 012/771] Simplify `eglot--protocol-initialize` * eglot.el (eglot--protocol-initialize): Simplify --- lisp/progmodes/eglot.el | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd91d7821ce..b59ee02ce3a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -70,7 +70,7 @@ "Point where next unread message starts") (eglot--define-process-var eglot--short-name nil - "A short name") + "A short name for the process") (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") @@ -81,6 +81,9 @@ (eglot--define-process-var eglot--events-buffer nil "A buffer pretty-printing the EGLOT RPC events") +(eglot--define-process-var eglot--capabilities :unreported + "Holds list of capabilities that server reported") + (cl-defmacro eglot--request (process method params @@ -367,29 +370,11 @@ :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) :textDocument (:synchronization (:didSave t)))) (lambda (&key capabilities) - (cl-destructuring-bind - (&rest all - &key - ;; capabilities reported by server - _textDocumentSync - _hoverProvider - _completionProvider - _definitionProvider - _referencesProvider - _documentHighlightProvider - _documentSymbolProvider - _workspaceSymbolProvider - _codeActionProvider - _documentFormattingProvider - _documentRangeFormattingProvider - _renameProvider - _executeCommandProvider - ) - capabilities - (when interactive + (setf (eglot--capabilities process) capabilities) + (when interactive (eglot--message "So yeah I got lots (%d) of capabilities" - (length all))))))) + (length capabilities)))))) (defun eglot-quit-server (process &optional sync) (interactive (list (eglot--current-process-or-lose))) From 5f1839bf171423424d64960af176b1d3c7ad3f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Aug 2017 16:53:40 +0100 Subject: [PATCH 013/771] Overhaul async mechanism safety --- lisp/progmodes/eglot.el | 170 ++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 76 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b59ee02ce3a..af9904a7b35 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -84,24 +84,8 @@ (eglot--define-process-var eglot--capabilities :unreported "Holds list of capabilities that server reported") -(cl-defmacro eglot--request (process - method - params - success-fn - &key - error-fn - timeout-fn - (async-p t)) - (append `(eglot--call-with-request - ,process - ,async-p - ,method - ,params - (cl-function ,success-fn)) - (and error-fn - `((cl-function ,error-fn))) - (and timeout-fn - `((cl-function ,timeout-fn))))) +(eglot--define-process-var eglot--moribund nil + "Non-nil if process is about to exit") (defun eglot--command (&optional errorp) (let ((probe (cdr (assoc major-mode eglot-executables)))) @@ -116,12 +100,12 @@ (interactive (list t)) (let ((project (project-current)) (command (eglot--command 'errorp))) - (unless project (eglot--error "Cannot work without a current project!")) + (unless project (eglot--error "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process))) (when (and current-process (process-live-p current-process)) - (eglot--message "Asking current process to terminate first") - (eglot-quit-server current-process 'sync))) + (eglot--message "(new-process) Asking current process to terminate first") + (eglot-quit-server current-process 'sync interactive))) (let* ((short-name (file-name-base (directory-file-name (car (project-roots (project-current)))))) @@ -154,20 +138,23 @@ (defun eglot--process-sentinel (process change) (with-current-buffer (process-buffer process) - (eglot--debug "Process state changed to %s" change) + (eglot--debug "(sentinel) Process state changed to %s" change) (when (not (process-live-p process)) ;; Remember to cancel all timers ;; - (maphash (lambda (id v) - (cl-destructuring-bind (_success _error timeout) v - (eglot--message "Cancelling timer for continuation %s" id) + (maphash (lambda (id triplet) + (cl-destructuring-bind (_success _error timeout) triplet + (eglot--message + "(sentinel) Cancelling timer for continuation %s" id) (cancel-timer timeout))) (eglot--pending-continuations process)) - (cond ((process-get process 'eglot--moribund) - (eglot--message "Process exited with status %s" + (cond ((eglot--moribund process) + (eglot--message "(sentinel) Moribund process exited with status %s" (process-exit-status process))) (t - (eglot--warn "Process unexpectedly changed to %s" change)))))) + (eglot--warn "(sentinel) Process unexpectedly changed to %s" + change))) + (delete-process process)))) (defun eglot--process-filter (proc string) (when (buffer-live-p (process-buffer proc)) @@ -306,35 +293,44 @@ (interactive (eglot--current-process-or-lose)) (clrhash (eglot--pending-continuations process))) -(defun eglot--call-with-request (process - async-p - method - params - success-fn - &optional error-fn timeout-fn) +(cl-defun eglot--request (process + method + params + &key success-fn error-fn timeout-fn (async-p t)) (let* ((id (eglot--next-request-id)) - (timeout-fn (or timeout-fn - (lambda () - (eglot--warn "Tired of waiting for reply to %s" id) - (remhash id (eglot--pending-continuations process))))) - (error-fn (or error-fn - (cl-function - (lambda (&key code message) - (eglot--warn "Request id=%s errored with code=%s: %s" - id code message))))) + (timeout-fn + (or timeout-fn + (lambda () + (eglot--warn + "(request) Tired of waiting for reply to %s" id) + (remhash id (eglot--pending-continuations process))))) + (error-fn + (or error-fn + (cl-function + (lambda (&key code message) + (eglot--warn + "(request) Request id=%s errored with code=%s: %s" + id code message))))) + (success-fn + (or success-fn + (cl-function + (lambda (&rest result-body) + (eglot--debug + "(request) Request id=%s replied to with result=%s: %s" + id result-body))))) (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) (eglot--process-send process - `(:jsonrpc "2.0" - :id ,id - :method ,method - :params ,params)) + `(:jsonrpc "2.0" + :id ,id + :method ,method + :params ,params)) (catch catch-tag (let ((timeout-timer (run-with-timer 5 nil (if async-p timeout-fn (lambda () - (throw catch-tag (apply timeout-fn))))))) + (throw catch-tag (funcall timeout-fn))))))) (puthash id (list (if async-p success-fn @@ -350,9 +346,18 @@ (unwind-protect (while t (unless (process-live-p process) - (eglot--error "Process %s died unexpectedly" process)) + (cond ((eglot--moribund process) + (throw catch-tag (delete-process process))) + (t + (eglot--error + "(request) Proc %s died unexpectedly during request with code %s" + process + (process-exit-status process))))) (accept-process-output nil 0.01)) - (cancel-timer timeout-timer))))))) + (when (memq timeout-timer timer-list) + (eglot--message + "(request) Last-change cancelling timer for continuation %s" id) + (cancel-timer timeout-timer)))))))) ;;; Requests @@ -363,38 +368,51 @@ :initialize `(:processId ,(emacs-pid) :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" - "file://" + ;; "file://" (expand-file-name (car (project-roots (project-current))))) :initializationOptions [] :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) :textDocument (:synchronization (:didSave t)))) - (lambda (&key capabilities) - (setf (eglot--capabilities process) capabilities) - (when interactive - (eglot--message - "So yeah I got lots (%d) of capabilities" - (length capabilities)))))) + :success-fn (cl-function + (lambda (&key capabilities) + (setf (eglot--capabilities process) capabilities) + (when interactive + (eglot--message + "So yeah I got lots (%d) of capabilities" + (length capabilities))))))) -(defun eglot-quit-server (process &optional sync) - (interactive (list (eglot--current-process-or-lose))) - (eglot--message "Asking server to terminate") - (eglot--request - process - :shutdown - nil - (lambda (&rest _anything) - (eglot--message "Now asking server to exit") - (process-put process 'eglot--moribund t) - (eglot--process-send process - `(:jsonrpc "2.0" - :method :exit))) - :async-p (not sync) - :timeout-fn (lambda () - (eglot--warn "Brutally deleting existing process %s" - process) - (process-put process 'eglot--moribund t) - (delete-process process)))) +(defun eglot-quit-server (process &optional sync interactive) + "Politely ask the server PROCESS to quit. +If SYNC, don't leave this function with the server still +running." + (interactive (list (eglot--current-process-or-lose) t t)) + (when interactive + (eglot--message "(eglot-quit-server) Asking %s politely to terminate" + process)) + (let ((brutal (lambda () + (eglot--warn "Brutally deleting existing process %s" + process) + (setf (eglot--moribund process) t) + (delete-process process)))) + (eglot--request + process + :shutdown + nil + :success-fn (lambda (&rest _anything) + (when interactive + (eglot--message "Now asking %s politely to exit" process)) + (setf (eglot--moribund process) t) + (eglot--request process + :exit + nil + :success-fn brutal + :async-p (not sync) + :error-fn brutal + :timeout-fn brutal)) + :error-fn brutal + :async-p (not sync) + :timeout-fn brutal))) ;;; Notifications From 0391fdf06219bb4bce7d7f4c81f7ff3df9d0ea1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 30 Apr 2018 13:38:37 +0100 Subject: [PATCH 014/771] Fix some byte-compilation warnings --- lisp/progmodes/eglot.el | 82 +++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index af9904a7b35..0a52da74faa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -27,6 +27,7 @@ (require 'json) (require 'cl-lib) (require 'project) +(require 'url-parse) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -211,8 +212,10 @@ (goto-char message-mark) (narrow-to-region message-mark message-end) - (eglot--process-receive proc (let ((json-object-type 'plist)) - (json-read))))) + (eglot--process-receive + proc + (let ((json-object-type 'plist)) + (json-read))))) (set-marker message-mark message-end) (setf (eglot--expected-bytes proc) nil))) (t @@ -258,21 +261,21 @@ (not continuations)) (eglot--warn "Ooops no continuation for id %s" response-id)) (continuations - (cancel-timer (third continuations)) + (cancel-timer (cl-third continuations)) (remhash response-id (eglot--pending-continuations)) (cond (err - (apply (second continuations) err)) + (apply (cl-second continuations) err)) (t - (apply (first continuations) (plist-get message :result))))) + (apply (cl-first continuations) (plist-get message :result))))) (t (let* ((method (plist-get message :method)) (handler-sym (intern (concat "eglot--" - method)))) + method)))) (if (functionp handler-sym) (apply handler-sym proc (plist-get message :params)) (eglot--debug "No implemetation for notification %s yet" - method))))))) + method))))))) (defvar eglot--expect-carriage-return nil) @@ -294,9 +297,9 @@ (clrhash (eglot--pending-continuations process))) (cl-defun eglot--request (process - method - params - &key success-fn error-fn timeout-fn (async-p t)) + method + params + &key success-fn error-fn timeout-fn (async-p t)) (let* ((id (eglot--next-request-id)) (timeout-fn (or timeout-fn @@ -363,6 +366,9 @@ ;;; Requests ;;; (defun eglot--protocol-initialize (process interactive) + "Initialize LSP protocol. +PROCESS is a connected process (network or local). +INTERACTIVE is t if caller was called interactively." (eglot--request process :initialize @@ -385,7 +391,7 @@ (defun eglot-quit-server (process &optional sync interactive) "Politely ask the server PROCESS to quit. If SYNC, don't leave this function with the server still -running." +running. INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t t)) (when interactive (eglot--message "(eglot-quit-server) Asking %s politely to terminate" @@ -437,47 +443,50 @@ running." (forward-line (plist-get pos-plist :line)) (forward-char (plist-get pos-plist :character)) (point)))) - (loop for diag across diagnostics - do (cl-destructuring-bind (&key range severity - _code _source message) - diag - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (pos-at start)) - (end-pos (pos-at end)) - (ov (make-overlay begin-pos - end-pos - buffer))) - (push ov eglot--diagnostic-overlays) - (overlay-put ov 'face - (case severity - (1 'flymake-errline) - (2 'flymake-warnline))) - (overlay-put ov 'help-echo - message) - (overlay-put ov 'eglot--diagnostic diag)))))))) + (cl-loop for diag across diagnostics + do (cl-destructuring-bind (&key range severity + _code _source message) + diag + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (pos-at start)) + (end-pos (pos-at end)) + (ov (make-overlay begin-pos + end-pos + buffer))) + (push ov eglot--diagnostic-overlays) + (overlay-put ov 'face + (cl-case severity + (1 'flymake-errline) + (2 'flymake-warnline))) + (overlay-put ov 'help-echo + message) + (overlay-put ov 'eglot--diagnostic diag)))))))) (t (eglot--message "OK so %s isn't visited" filename))))) ;;; Helpers ;;; -(defun - eglot--debug (format &rest args) +(defun eglot--debug (format &rest args) + "Debug message FORMAT with ARGS." (display-warning 'eglot - (apply #'format format args) - :debug)) + (apply #'format format args) + :debug)) (defun eglot--error (format &rest args) + "Error out with FORMAT with ARGS." (error (apply #'format format args))) (defun eglot--message (format &rest args) + "Message out with FORMAT with ARGS." (message (concat "[eglot] " (apply #'format format args)))) (defun eglot--warn (format &rest args) + "Warning message with FORMAT and ARGS." (display-warning 'eglot - (apply #'format format args) - :warning)) + (apply #'format format args) + :warning)) @@ -504,6 +513,7 @@ running." (put 'eglot--mode-line-format 'risky-local-variable t) (defun eglot--mode-line-format () + "Compose the mode-line format spec." (let* ((proc (eglot--current-process)) (name (and proc (process-live-p proc) From 2b972ba05be14b8a2ac81dc6af004c153437d0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 30 Apr 2018 13:39:16 +0100 Subject: [PATCH 015/771] Fix mode line * eglot.el (mode-line-misc-info): conditionalize to eglot-mode --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a52da74faa..8946692c9e7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -572,7 +572,7 @@ running. INTERACTIVE is t if called interactively." map))))))) (add-to-list 'mode-line-misc-info - `(t + `(eglot-mode (" [" eglot--mode-line-format "] "))) (provide 'eglot) From b84c05058984f17cfc3467b94b445781c763ca32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 30 Apr 2018 18:54:54 +0100 Subject: [PATCH 016/771] Start working on this again * eglot.el (url-util): Require it. (eglot--process-sentinel): pending continuations now are quads (added env). (eglot--process-filter): Unwind message markers correctly if handling fails. (eglot--obj): Simple macro. (eglot--log-event): Add some info to logged event. (eglot--environment-vars, eglot--environment): Helper vars. (eglot--process-receive): Improve. (eglot--process-send): Niver log. (eglot--request): Use eglot--obj. Add environment. (eglot--notify): New helper. (eglot--protocol-initialize): RLS must like file:// (eglot--current-flymake-report-fn): New var. (eglot--textDocument/publishDiagnostics): Use flymake from Emacs 26. (eglot-mode): Proper minor mode. (eglot--recent-changes, eglot--versioned-identifier): New stuff. (eglot--current-buffer-versioned-identifier) (eglot--current-buffer-VersionedTextDocumentIdentifier) (eglot--current-buffer-TextDocumentItem, eglot--after-change) (eglot--signalDidOpen, eglot--maybe-signal-didChange): New stuff. (eglot-flymake-backend): More or less a flymake backend function. --- lisp/progmodes/eglot.el | 267 ++++++++++++++++++++++++++++++---------- 1 file changed, 204 insertions(+), 63 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8946692c9e7..8da6267123a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -28,6 +28,7 @@ (require 'cl-lib) (require 'project) (require 'url-parse) +(require 'url-util) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -143,8 +144,8 @@ (when (not (process-live-p process)) ;; Remember to cancel all timers ;; - (maphash (lambda (id triplet) - (cl-destructuring-bind (_success _error timeout) triplet + (maphash (lambda (id quad) + (cl-destructuring-bind (_success _error timeout _env) quad (eglot--message "(sentinel) Cancelling timer for continuation %s" id) (cancel-timer timeout))) @@ -185,7 +186,7 @@ (when new-expected-bytes (when expected-bytes (eglot--warn - (concat "Unexpectedly starting new message but %s bytes" + (concat "Unexpectedly starting new message but %s bytes " "reportedly remaining from previous one") expected-bytes)) (set-marker message-mark (point)) @@ -207,22 +208,29 @@ (let* ((message-end (byte-to-position (+ (position-bytes message-mark) expected-bytes)))) - (save-excursion - (save-restriction - (goto-char message-mark) - (narrow-to-region message-mark - message-end) - (eglot--process-receive - proc - (let ((json-object-type 'plist)) - (json-read))))) - (set-marker message-mark message-end) - (setf (eglot--expected-bytes proc) nil))) + (unwind-protect + (save-excursion + (save-restriction + (goto-char message-mark) + (narrow-to-region message-mark + message-end) + (eglot--process-receive + proc + (let ((json-object-type 'plist)) + (json-read))))) + (set-marker message-mark message-end) + (setf (eglot--expected-bytes proc) nil)))) (t ;; just adding some stuff to the end that doesn't yet ;; complete the message ))))))) +(defmacro eglot--obj (&rest what) + "Make an object suitable for `json-encode'" + ;; FIXME: maybe later actually do something, for now this just fixes + ;; the indenting of literal plists. + `(list ,@what)) + (defun eglot-events-buffer (process &optional interactive) (interactive (list (eglot--current-process-or-lose) t)) (let* ((probe (eglot--events-buffer process)) @@ -241,22 +249,39 @@ (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc type message) +(defun eglot--log-event (proc type message id error) (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) (goto-char (point-max)) - (insert (format "%s: \n%s\n" type (pp-to-string message)))))) + (insert (format "%s%s%s:\n%s\n" + type + (if id (format " (id:%s)" id) "") + (if error " ERROR" "") + (pp-to-string message)))))) + +(defvar eglot--environment-vars + '(eglot--current-flymake-report-fn) + "A list of variables with saved values on every request.") + +(defvar eglot--environment nil + "Dynamically bound alist of symbol and values") (defun eglot--process-receive (proc message) - (let ((inhibit-read-only t)) - (insert (format "Server said:\n%s\n" message))) - (eglot--log-event proc 'server message) - ;; Maybe this is a responsee - ;; + "Process MESSAGE from PROC." (let* ((response-id (plist-get message :id)) (err (plist-get message :error)) (continuations (and response-id (gethash response-id (eglot--pending-continuations))))) + (eglot--log-event proc + (cond ((not response-id) + 'server-notification) + ((not continuations) + 'unexpected-server-reply) + (t + 'server-reply)) + message + response-id + err) (cond ((and response-id (not continuations)) (eglot--warn "Ooops no continuation for id %s" response-id)) @@ -271,21 +296,28 @@ (t (let* ((method (plist-get message :method)) (handler-sym (intern (concat "eglot--" - method)))) + method))) + (eglot--environment (cl-fourth continuations))) (if (functionp handler-sym) - (apply handler-sym proc (plist-get message :params)) + (cl-progv + (mapcar #'car eglot--environment) + (mapcar #'cdr eglot--environment) + (apply handler-sym proc (plist-get message :params))) (eglot--debug "No implemetation for notification %s yet" method))))))) (defvar eglot--expect-carriage-return nil) -(defun eglot--process-send (proc message) +(defun eglot--process-send (id proc message) (let* ((json (json-encode message)) (to-send (format "Content-Length: %d\r\n\r\n%s" (string-bytes json) json))) (process-send-string proc to-send) - (eglot--log-event proc 'client message))) + (eglot--log-event proc (if id + 'client-request + 'client-notification) + message id nil))) (defvar eglot--next-request-id 0) @@ -300,6 +332,7 @@ method params &key success-fn error-fn timeout-fn (async-p t)) + "Make a request to PROCESS, expecting a reply." (let* ((id (eglot--next-request-id)) (timeout-fn (or timeout-fn @@ -322,11 +355,12 @@ "(request) Request id=%s replied to with result=%s: %s" id result-body))))) (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) - (eglot--process-send process - `(:jsonrpc "2.0" - :id ,id - :method ,method - :params ,params)) + (eglot--process-send id + process + (eglot--obj :jsonrpc "2.0" + :id id + :method method + :params params)) (catch catch-tag (let ((timeout-timer (run-with-timer 5 nil @@ -343,7 +377,9 @@ error-fn (lambda (&rest args) (throw catch-tag (apply error-fn args)))) - timeout-timer) + timeout-timer + (cl-loop for var in eglot--environment-vars + collect (cons var (symbol-value var)))) (eglot--pending-continuations process)) (unless async-p (unwind-protect @@ -362,6 +398,15 @@ "(request) Last-change cancelling timer for continuation %s" id) (cancel-timer timeout-timer)))))))) +(cl-defun eglot--notify (process method params) + "Notify PROCESS of something, don't expect a reply.e" + (eglot--process-send nil + process + (eglot--obj :jsonrpc "2.0" + :id nil + :method method + :params params))) + ;;; Requests ;;; @@ -373,8 +418,7 @@ INTERACTIVE is t if caller was called interactively." process :initialize `(:processId ,(emacs-pid) - :rootPath ,(concat "" ;; FIXME RLS doesn't like "file://" - ;; "file://" + :rootPath ,(concat "file://" (expand-file-name (car (project-roots (project-current))))) :initializationOptions [] @@ -423,45 +467,54 @@ running. INTERACTIVE is t if called interactively." ;;; Notifications ;;; -(defvar-local eglot--diagnostic-overlays nil) +(defvar eglot--current-flymake-report-fn nil) (cl-defun eglot--textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" (let* ((obj (url-generic-parse-url uri)) (filename (car (url-path-and-query obj))) - (buffer (find-buffer-visiting filename))) + (buffer (find-buffer-visiting filename)) + (report-fn (cdr (assoc 'eglot--current-flymake-report-fn + eglot--environment)))) (cond + ((not eglot--current-flymake-report-fn) + (eglot--warn "publishDiagnostics called but no report-fn")) + ((and report-fn + (not (eq report-fn + eglot--current-flymake-report-fn))) + (eglot--warn "outdated publishDiagnostics report from server")) (buffer (with-current-buffer buffer (eglot--message "OK so add some %s diags" (length diagnostics)) - (mapc #'delete-overlay eglot--diagnostic-overlays) - (setq eglot--diagnostic-overlays nil) - (cl-flet ((pos-at (pos-plist) - (save-excursion - (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) - (forward-char (plist-get pos-plist :character)) - (point)))) - (cl-loop for diag across diagnostics - do (cl-destructuring-bind (&key range severity - _code _source message) - diag - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (pos-at start)) - (end-pos (pos-at end)) - (ov (make-overlay begin-pos - end-pos - buffer))) - (push ov eglot--diagnostic-overlays) - (overlay-put ov 'face - (cl-case severity - (1 'flymake-errline) - (2 'flymake-warnline))) - (overlay-put ov 'help-echo - message) - (overlay-put ov 'eglot--diagnostic diag)))))))) + (cl-flet ((pos-at + (pos-plist) + (car (flymake-diag-region + (current-buffer) + (plist-get pos-plist :line) + (plist-get pos-plist :character))))) + (cl-loop for diag-spec across diagnostics + collect (cl-destructuring-bind (&key range severity + _code _source message) + diag-spec + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (pos-at start)) + (end-pos (pos-at end))) + (flymake-make-diagnostic + (current-buffer) + begin-pos end-pos + (cond ((<= severity 1) + :error) + ((= severity 2) + :warning) + (t + :note)) + message)))) + into diags + finally (funcall + eglot--current-flymake-report-fn + diags))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -498,7 +551,19 @@ running. INTERACTIVE is t if called interactively." :group 'eglot) (define-minor-mode eglot-mode - "Minor mode for buffers where EGLOT is possible") + "Minor mode for buffers where EGLOT is possible" + nil + nil + eglot-mode-map + (cond (eglot-mode + (add-hook 'after-change-functions 'eglot--after-change nil t) + (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) + (if (eglot--current-process) + (eglot--signalDidOpen) + (eglot--warn "No process"))) + (t + (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) + (remove-hook 'after-change-functions 'eglot--after-change t)))) (defvar eglot-menu) @@ -575,5 +640,81 @@ running. INTERACTIVE is t if called interactively." `(eglot-mode (" [" eglot--mode-line-format "] "))) +(defvar eglot--recent-changes nil + "List of recent changes as collected by `eglot--after-change'") + +(defvar-local eglot--versioned-identifier 0) + +(defun eglot--current-buffer-versioned-identifier () + "Return a VersionedTextDocumentIdentifier." + ;; FIXME: later deal with workspaces + eglot--versioned-identifier) + +(defun eglot--current-buffer-VersionedTextDocumentIdentifier () + (eglot--obj :uri + (concat "file://" + (url-hexify-string + (file-truename buffer-file-name) + url-path-allowed-chars)) + :version (eglot--current-buffer-versioned-identifier))) + +(defun eglot--current-buffer-TextDocumentItem () + (append + (eglot--current-buffer-VersionedTextDocumentIdentifier) + (eglot--obj :languageId (cdr (assoc major-mode + '((rust-mode . rust) + (emacs-lisp-mode . emacs-lisp)))) + :text + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max)))))) + +(defun eglot--after-change (start end length) + (cl-incf eglot--versioned-identifier) + (push (list start end length) eglot--recent-changes) + (eglot--message "start is %s, end is %s, length is %s" start end length)) + +(defun eglot--signalDidOpen () + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didOpen + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) + +(defun eglot--maybe-signal-didChange () + (when eglot--recent-changes + (save-excursion + (save-restriction + (widen) + (let* ((start (cl-reduce #'min (mapcar #'car eglot--recent-changes))) + (end (cl-reduce #'max (mapcar #'cadr eglot--recent-changes)))) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didChange + (eglot--obj + :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (vector + (eglot--obj + :range (eglot--obj + :start + (eglot--obj :line + (line-number-at-pos start t) + :character + (- (goto-char start) + (line-beginning-position))) + :end + (eglot--obj :line + (line-number-at-pos end t) + :character + (- (goto-char end) + (line-beginning-position)))) + :rangeLength (- end start) + :text (buffer-substring-no-properties start end)))))))) + (setq eglot--recent-changes nil))) + +(defun eglot-flymake-backend (report-fn &rest _more) + (setq eglot--current-flymake-report-fn report-fn) + (eglot--maybe-signal-didChange)) + (provide 'eglot) ;;; eglot.el ends here From 2ff4dff73de109615eb5697ac6effae53483e276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 1 May 2018 10:14:53 +0100 Subject: [PATCH 017/771] * eglot.el (eglot-mode-map): move up before minor mode. --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8da6267123a..8356f8ef3f9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -550,6 +550,8 @@ running. INTERACTIVE is t if called interactively." "Face for package-name in EGLOT's mode line." :group 'eglot) +(defvar eglot-mode-map (make-sparse-keymap)) + (define-minor-mode eglot-mode "Minor mode for buffers where EGLOT is possible" nil @@ -567,9 +569,7 @@ running. INTERACTIVE is t if called interactively." (defvar eglot-menu) -(defvar eglot-mode-map (make-sparse-keymap)) - -(easy-menu-define eglot-menu eglot-mode-map "SLY" +(easy-menu-define eglot-menu eglot-mode-map "EGLOT" `("EGLOT" )) (defvar eglot--mode-line-format From 76dd0850a71764d17bb72d243c5744ae4ef08df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 1 May 2018 10:18:31 +0100 Subject: [PATCH 018/771] Doc fixes * eglot.el (eglot-mode-map): Move up before minor mode. --- lisp/progmodes/eglot.el | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8356f8ef3f9..9e5f6c2818f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -36,12 +36,12 @@ :group 'applications) (defvar eglot-executables '((rust-mode . ("rls"))) - "Alist mapping major modes to server executables") + "Alist mapping major modes to server executables.") (defvar eglot--processes-by-project (make-hash-table :test #'equal)) (defun eglot--current-process () - "The current logical EGLOT process" + "The current logical EGLOT process." (let ((cur (project-current))) (and cur (gethash cur eglot--processes-by-project)))) @@ -98,7 +98,8 @@ probe)) (defun eglot-new-process (&optional interactive) - "Starts a new EGLOT process and initializes it" + "Start a new EGLOT process and initialize it. +INTERACTIVE is t if called interactively." (interactive (list t)) (let ((project (project-current)) (command (eglot--command 'errorp))) @@ -226,12 +227,14 @@ ))))))) (defmacro eglot--obj (&rest what) - "Make an object suitable for `json-encode'" + "Make WHAT a suitable argument for `json-encode'." ;; FIXME: maybe later actually do something, for now this just fixes ;; the indenting of literal plists. `(list ,@what)) (defun eglot-events-buffer (process &optional interactive) + "Display events buffer for current LSP connection PROCESS. +INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) (let* ((probe (eglot--events-buffer process)) (buffer (or (and (buffer-live-p probe) @@ -264,7 +267,7 @@ "A list of variables with saved values on every request.") (defvar eglot--environment nil - "Dynamically bound alist of symbol and values") + "Dynamically bound alist of symbol and values.") (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." @@ -325,6 +328,7 @@ (setq eglot--next-request-id (1+ eglot--next-request-id))) (defun eglot-forget-pending-continuations (process) + "Stop waiting for responses from the current LSP PROCESS." (interactive (eglot--current-process-or-lose)) (clrhash (eglot--pending-continuations process))) @@ -641,7 +645,7 @@ running. INTERACTIVE is t if called interactively." (" [" eglot--mode-line-format "] "))) (defvar eglot--recent-changes nil - "List of recent changes as collected by `eglot--after-change'") + "List of recent changes as collected by `eglot--after-change'.") (defvar-local eglot--versioned-identifier 0) From 69a3abdd0f0b7118f8ebce60e339391c66f314fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 1 May 2018 10:46:06 +0100 Subject: [PATCH 019/771] Lay groundwork for uniform treatment of network connections * eglot.el (eglot--connect): New helper. (eglot-new-process): Use it. (pcase): Require it. --- lisp/progmodes/eglot.el | 62 ++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9e5f6c2818f..cd213e72c92 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -29,6 +29,7 @@ (require 'project) (require 'url-parse) (require 'url-util) +(require 'pcase) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -97,12 +98,27 @@ major-mode)) probe)) +(defun eglot--connect (name filter sentinel) + "Helper for `eglot-new-process'. +NAME is a name to give the inferior process or connection. +FILTER and SENTINEL are filter and sentinel. +Should return a list of (PROCESS BUFFER)." + (let ((proc (make-process :name name + :buffer (get-buffer-create + (format "*%s inferior*" name)) + :command (eglot--command 'error) + :connection-type 'pipe + :filter filter + :sentinel sentinel + :stderr (get-buffer-create (format "*%s stderr*" + name))))) + (list proc (process-buffer proc)))) + (defun eglot-new-process (&optional interactive) "Start a new EGLOT process and initialize it. INTERACTIVE is t if called interactively." (interactive (list t)) - (let ((project (project-current)) - (command (eglot--command 'errorp))) + (let ((project (project-current))) (unless project (eglot--error "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process))) (when (and current-process @@ -114,30 +130,24 @@ INTERACTIVE is t if called interactively." (car (project-roots (project-current)))))) (good-name (format "EGLOT server (%s)" short-name))) - (with-current-buffer (get-buffer-create - (format "*%s inferior*" good-name)) - (let* ((proc - (make-process :name good-name - :buffer (current-buffer) - :command command - :connection-type 'pipe - :filter 'eglot--process-filter - :sentinel 'eglot--process-sentinel - :stderr (get-buffer-create (format "*%s stderr*" - good-name)))) - (inhibit-read-only t)) - (setf (eglot--short-name proc) short-name) - (puthash (project-current) proc eglot--processes-by-project) - (erase-buffer) - (let ((marker (point-marker))) - (set-marker-insertion-type marker nil) - (setf (eglot--message-mark proc) marker)) - (read-only-mode t) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert - (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize proc interactive)))))) + (pcase-let ((`(,proc ,buffer) + (eglot--connect good-name + 'eglot--process-filter + 'eglot--process-sentinel))) + (with-current-buffer buffer + (let ((inhibit-read-only t)) + (setf (eglot--short-name proc) short-name) + (puthash (project-current) proc eglot--processes-by-project) + (erase-buffer) + (let ((marker (point-marker))) + (set-marker-insertion-type marker nil) + (setf (eglot--message-mark proc) marker)) + (read-only-mode t) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (insert + (format "\n-----------------------------------\n")))) + (eglot--protocol-initialize proc interactive))))))) (defun eglot--process-sentinel (process change) (with-current-buffer (process-buffer process) From 09dfb21d3e0511a4bf2935be53dfb1097d0f7831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 1 May 2018 22:30:09 +0100 Subject: [PATCH 020/771] Fix parser to accept multiple messages in one chunk * eglot.el (eglot--process-filter): Redesign slightly. (eglot--message-mark): Remove. don't need this. --- lisp/progmodes/eglot.el | 126 ++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 64 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd213e72c92..ab13b3a52a9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -69,9 +69,6 @@ `(let ((proc (or ,process (eglot--current-process-or-lose)))) (process-put proc ',prop ,to-store)))))) -(eglot--define-process-var eglot--message-mark nil - "Point where next unread message starts") - (eglot--define-process-var eglot--short-name nil "A short name for the process") @@ -139,9 +136,6 @@ INTERACTIVE is t if called interactively." (setf (eglot--short-name proc) short-name) (puthash (project-current) proc eglot--processes-by-project) (erase-buffer) - (let ((marker (point-marker))) - (set-marker-insertion-type marker nil) - (setf (eglot--message-mark proc) marker)) (read-only-mode t) (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) @@ -170,71 +164,75 @@ INTERACTIVE is t if called interactively." (delete-process process)))) (defun eglot--process-filter (proc string) + "Called when new data STRING has arrived for PROC." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) - (let ((moving (= (point) (process-mark proc))) - (inhibit-read-only t) + (let ((inhibit-read-only t) (pre-insertion-mark (copy-marker (process-mark proc))) - (expected-bytes (eglot--expected-bytes proc)) - (message-mark (eglot--message-mark proc))) - (save-excursion - ;; Insert the text, advancing the process marker. - (goto-char (process-mark proc)) - (insert string) - (set-marker (process-mark proc) (point))) - (if moving (goto-char (process-mark proc))) + (expected-bytes (eglot--expected-bytes proc))) + ;; Insert the text, advancing the process marker. + (goto-char (process-mark proc)) + (insert string) + (set-marker (process-mark proc) (point)) - ;; check for new message header + ;; goto point just before insertion ;; - (save-excursion - (goto-char pre-insertion-mark) - (let* ((match (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t)) - (new-expected-bytes (and match - (string-to-number (match-string 1))))) - (when new-expected-bytes - (when expected-bytes - (eglot--warn - (concat "Unexpectedly starting new message but %s bytes " - "reportedly remaining from previous one") - expected-bytes)) - (set-marker message-mark (point)) - (setf (eglot--expected-bytes proc) new-expected-bytes) - (setq expected-bytes new-expected-bytes)))) + (goto-char pre-insertion-mark) - ;; check for message body + ;; loop for each message (more than one might have arrived) ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes message-mark)))) - (cond ((not expected-bytes) - (eglot--warn - "Skipping %s bytes of unexpected garbage from process %s" - available-bytes - proc) - (set-marker message-mark (process-mark proc))) - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes message-mark) - expected-bytes)))) - (unwind-protect - (save-excursion - (save-restriction - (goto-char message-mark) - (narrow-to-region message-mark - message-end) - (eglot--process-receive - proc - (let ((json-object-type 'plist)) - (json-read))))) - (set-marker message-mark message-end) - (setf (eglot--expected-bytes proc) nil)))) - (t - ;; just adding some stuff to the end that doesn't yet - ;; complete the message - ))))))) + (catch 'done + (while t + (let* ((match (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" + (+ (point) 100) + t)) + (new-expected-bytes (and match + (string-to-number (match-string 1))))) + (when new-expected-bytes + (when expected-bytes + (eglot--warn + (concat "Unexpectedly starting new message but %s bytes " + "reportedly remaining from previous one") + expected-bytes)) + (setf (eglot--expected-bytes proc) new-expected-bytes) + (setq expected-bytes new-expected-bytes))) + + ;; check for message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond ((not expected-bytes) ; previous search didn't match + (eglot--warn + "Skipping %s bytes of unexpected garbage from process %s" + available-bytes + proc) + (goto-char (process-mark proc)) + (throw 'done :skipping-garbage)) + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) + message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; process in another buffer, shielding + ;; buffer from tamper + (with-temp-buffer + (eglot--process-receive proc json-message)))) + (goto-char message-end) + (setf (eglot--expected-bytes proc) nil + expected-bytes nil))) + (when (= (point) (process-mark proc)) + (throw 'done :clean-done))) + (t + ;; just adding some stuff to the end that doesn't yet + ;; complete the message + (throw 'done :waiting-for-more-bytes)))))))))) (defmacro eglot--obj (&rest what) "Make WHAT a suitable argument for `json-encode'." From c170dbedf8147a0bc75b0f22c7a2ef52f8611872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 1 May 2018 22:47:09 +0100 Subject: [PATCH 021/771] Implement spinners and rls's window/progress * eglot.el (eglot--window/progress): New. (eglot--mode-line-format): Rework. (eglot--snpinner): New var. (compile): require it. --- lisp/progmodes/eglot.el | 107 ++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ab13b3a52a9..e5d69379a4d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -30,6 +30,7 @@ (require 'url-parse) (require 'url-util) (require 'pcase) +(require 'compile) ; for some faces (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -87,6 +88,10 @@ (eglot--define-process-var eglot--moribund nil "Non-nil if process is about to exit") +(eglot--define-process-var eglot--spinner `(nil nil t) + "\"Spinner\" used by some servers. +A list (ID WHAT DONE-P).") + (defun eglot--command (&optional errorp) (let ((probe (cdr (assoc major-mode eglot-executables)))) (unless (or (not errorp) @@ -591,17 +596,15 @@ running. INTERACTIVE is t if called interactively." (defun eglot--mode-line-format () "Compose the mode-line format spec." - (let* ((proc (eglot--current-process)) - (name (and proc - (process-live-p proc) - (eglot--short-name proc))) - (pending (and proc - (hash-table-count - (eglot--pending-continuations proc)))) - (format-number (lambda (n) (cond ((and n (not (zerop n))) - (format "%d" n)) - (n "-") - (t "*"))))) + (pcase-let* ((proc (eglot--current-process)) + (name (and proc + (process-live-p proc) + (eglot--short-name proc))) + (pending (and proc + (hash-table-count + (eglot--pending-continuations proc)))) + (`(,_id ,what ,done-p) (and proc + (eglot--spinner)))) (append `((:propertize "eglot" face eglot-mode-line @@ -612,41 +615,51 @@ running. INTERACTIVE is t if called interactively." mouse-face mode-line-highlight help-echo "mouse-1: pop-up EGLOT menu" )) - (if name - `(" " - (:propertize - ,name - face eglot-mode-line - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] 'eglot-events-buffer) - (define-key map [mode-line mouse-2] 'eglot-quit-server) - (define-key map [mode-line mouse-3] 'eglot-new-process) - map) - mouse-face mode-line-highlight - help-echo ,(concat "mouse-1: events buffer\n" - "mouse-2: quit server\n" - "mouse-3: new process")) - "/" - (:propertize - ,(funcall format-number pending) - help-echo ,(if name - (format - "%s pending events outgoing\n%s" + (when name + `(":" + (:propertize + ,name + face eglot-mode-line + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] 'eglot-events-buffer) + (define-key map [mode-line mouse-2] 'eglot-quit-server) + (define-key map [mode-line mouse-3] 'eglot-new-process) + map) + mouse-face mode-line-highlight + help-echo ,(concat "mouse-1: go to events buffer\n" + "mouse-2: quit server\n" + "mouse-3: new process")) + ,@(when (and what (not done-p)) + `("/" + (:propertize + ,what + help-echo ,(concat "mouse-1: go to events buffer") + mouse-face mode-line-highlight + face compilation-mode-line-run + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] + 'eglot-events-buffer) + map)))) + ,@(when (cl-plusp pending) + `("/" + (:propertize + (format "%d" pending) + help-echo ,(format + "%s unanswered requests\n%s" pending (concat "mouse-1: go to events buffer" "mouse-3: forget pending continuations")) - "No current connection") - mouse-face mode-line-highlight - face ,(cond ((and pending (cl-plusp pending)) - 'warning) - (t - 'eglot-mode-line)) - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - 'eglot-events-buffer) - (define-key map [mode-line mouse-3] - 'eglot-forget-pending-continuations) - map))))))) + mouse-face mode-line-highlight + face ,(cond ((and pending (cl-plusp pending)) + 'warning) + (t + 'eglot-mode-line)) + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] + 'eglot-events-buffer) + (define-key map [mode-line mouse-3] + 'eglot-forget-pending-continuations) + map))))))))) (add-to-list 'mode-line-misc-info `(eglot-mode @@ -728,5 +741,13 @@ running. INTERACTIVE is t if called interactively." (setq eglot--current-flymake-report-fn report-fn) (eglot--maybe-signal-didChange)) + +;;; Rust-specific +;;; +(cl-defun eglot--window/progress + (process &key id done title ) + "Handle notification window/progress" + (setf (eglot--spinner process) (list id title done))) + (provide 'eglot) ;;; eglot.el ends here From 235574be1574d9857c24aed97685dec49a2dbed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 1 May 2018 23:13:49 +0100 Subject: [PATCH 022/771] Report server status in the mode-line * eglot.el (eglot--status): New var. (eglot--log-event): Try to be more useful for other stuff. (eglot--protocol-initialize): Set status to nil on successful connect. (eglot--window/showMessage): Set status to error if needed. (eglot--mode-line-format): Display status if serious. --- lisp/progmodes/eglot.el | 57 +++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e5d69379a4d..44023b721d0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -92,6 +92,10 @@ "\"Spinner\" used by some servers. A list (ID WHAT DONE-P).") +(eglot--define-process-var eglot--status `(:unknown nil) + "Status as declared by the server. +A list (WHAT SERIOUS-P).") + (defun eglot--command (&optional errorp) (let ((probe (cdr (assoc major-mode eglot-executables)))) (unless (or (not errorp) @@ -265,15 +269,22 @@ INTERACTIVE is t if called interactively." (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc type message id error) +(defun eglot--log-event (proc type message &optional id error) + "Log an eglot-related event. +PROC is the current process. TYPE is an identifier. MESSAGE is +a JSON-like plist or anything else. ID is a continuation +identifier. ERROR is non-nil if this is an error." (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) (goto-char (point-max)) - (insert (format "%s%s%s:\n%s\n" - type - (if id (format " (id:%s)" id) "") - (if error " ERROR" "") - (pp-to-string message)))))) + (let ((msg (format "%s%s%s:\n%s\n" + type + (if id (format " (id:%s)" id) "") + (if error " ERROR" "") + (pp-to-string message)))) + (when error + (setq msg (propertize msg 'face 'error))) + (insert msg))))) (defvar eglot--environment-vars '(eglot--current-flymake-report-fn) @@ -445,6 +456,7 @@ INTERACTIVE is t if caller was called interactively." (lambda (&key capabilities) (setf (eglot--capabilities process) capabilities) (when interactive + (setf (eglot--status process) nil) (eglot--message "So yeah I got lots (%d) of capabilities" (length capabilities))))))) @@ -535,6 +547,16 @@ running. INTERACTIVE is t if called interactively." (t (eglot--message "OK so %s isn't visited" filename))))) +(cl-defun eglot--window/showMessage + (process &key type message) + "Handle notification window/showMessage" + (when (<= 1 type) + (setf (eglot--status process) '("error" t)) + (eglot--log-event process + (propertize "server-error" 'face 'error) + message)) + (eglot--message "Server reports (type=%s): %s" type message)) + ;;; Helpers ;;; @@ -603,8 +625,12 @@ running. INTERACTIVE is t if called interactively." (pending (and proc (hash-table-count (eglot--pending-continuations proc)))) - (`(,_id ,what ,done-p) (and proc - (eglot--spinner)))) + (`(,_id ,doing ,done-p) + (and proc + (eglot--spinner proc))) + (`(,status ,serious-p) + (and proc + (eglot--status proc)))) (append `((:propertize "eglot" face eglot-mode-line @@ -629,10 +655,21 @@ running. INTERACTIVE is t if called interactively." help-echo ,(concat "mouse-1: go to events buffer\n" "mouse-2: quit server\n" "mouse-3: new process")) - ,@(when (and what (not done-p)) + ,@(when serious-p `("/" (:propertize - ,what + ,status + help-echo ,(concat "mouse-1: go to events buffer") + mouse-face mode-line-highlight + face compilation-mode-line-fail + keymap ,(let ((map (make-sparse-keymap))) + (define-key map [mode-line mouse-1] + 'eglot-events-buffer) + map)))) + ,@(when (and doing (not done-p)) + `("/" + (:propertize + ,doing help-echo ,(concat "mouse-1: go to events buffer") mouse-face mode-line-highlight face compilation-mode-line-run From 2c2aec71fb5079a2075627a09de748c4fd57fa9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 12:04:56 +0100 Subject: [PATCH 023/771] Don't switch to possibly dead buffer in sentinel * eglot.el (eglot--process-sentinel): Don't with-current-buffer. --- lisp/progmodes/eglot.el | 44 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 44023b721d0..22ff0318ca6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -153,24 +153,23 @@ INTERACTIVE is t if called interactively." (eglot--protocol-initialize proc interactive))))))) (defun eglot--process-sentinel (process change) - (with-current-buffer (process-buffer process) - (eglot--debug "(sentinel) Process state changed to %s" change) - (when (not (process-live-p process)) - ;; Remember to cancel all timers - ;; - (maphash (lambda (id quad) - (cl-destructuring-bind (_success _error timeout _env) quad - (eglot--message - "(sentinel) Cancelling timer for continuation %s" id) - (cancel-timer timeout))) - (eglot--pending-continuations process)) - (cond ((eglot--moribund process) - (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status process))) - (t - (eglot--warn "(sentinel) Process unexpectedly changed to %s" - change))) - (delete-process process)))) + (eglot--debug "(sentinel) Process state changed to %s" change) + (when (not (process-live-p process)) + ;; Remember to cancel all timers + ;; + (maphash (lambda (id quad) + (cl-destructuring-bind (_success _error timeout _env) quad + (eglot--message + "(sentinel) Cancelling timer for continuation %s" id) + (cancel-timer timeout))) + (eglot--pending-continuations process)) + (cond ((eglot--moribund process) + (eglot--message "(sentinel) Moribund process exited with status %s" + (process-exit-status process))) + (t + (eglot--warn "(sentinel) Process unexpectedly changed to %s" + change))) + (delete-process process))) (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." @@ -371,7 +370,8 @@ identifier. ERROR is non-nil if this is an error." (error-fn (or error-fn (cl-function - (lambda (&key code message) + (lambda (&key data code message &allow-other-keys) + (setf (eglot--status process) '("error" t)) (eglot--warn "(request) Request id=%s errored with code=%s: %s" id code message))))) @@ -446,9 +446,9 @@ INTERACTIVE is t if caller was called interactively." process :initialize `(:processId ,(emacs-pid) - :rootPath ,(concat "file://" - (expand-file-name (car (project-roots - (project-current))))) + :rootPath ,(concat + (expand-file-name (car (project-roots + (project-current))))) :initializationOptions [] :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) :textDocument (:synchronization (:didSave t)))) From 7d6547dfa4c2a66597c0ac2950c4f93875bec884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 12:05:47 +0100 Subject: [PATCH 024/771] Start experimenting with python * eglot.el (eglot-executables): Add pyls. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22ff0318ca6..ff71a2f782f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -37,7 +37,8 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-executables '((rust-mode . ("rls"))) +(defvar eglot-executables '((rust-mode . ("rls")) + (python-mode . ("pyls"))) "Alist mapping major modes to server executables.") (defvar eglot--processes-by-project (make-hash-table :test #'equal)) From dc5b0eb42afecfa23ec3e37114978487399159e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 13:28:18 +0100 Subject: [PATCH 025/771] Auto update mode-line after setting some process properties * eglot.el (eglot--define-process-var): Rework. (eglot--short-name, eglot--spinner, eglot--status): Update mode-line after setting it. --- lisp/progmodes/eglot.el | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ff71a2f782f..1a82cc507d3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -55,7 +55,12 @@ (if (project-current) "" " (Also no current project)")))) -(defmacro eglot--define-process-var (var-sym initval &optional doc) +(defmacro eglot--define-process-var + (var-sym initval &optional doc mode-line-update-p) + "Define VAR-SYM as a generalized process-local variable. +INITVAL is the default value. DOC is the documentation. +MODE-LINE-UPDATE-P says to also force a mode line update +after setting it." (declare (indent 2)) `(progn (put ',var-sym 'function-documentation ,doc) @@ -67,12 +72,15 @@ (process-put proc ',var-sym def) def)))) (gv-define-setter ,var-sym (to-store &optional process) - (let ((prop ',var-sym)) - `(let ((proc (or ,process (eglot--current-process-or-lose)))) - (process-put proc ',prop ,to-store)))))) + (let* ((prop ',var-sym)) + ,(let ((form '(let ((proc (or ,process (eglot--current-process-or-lose)))) + (process-put proc ',prop ,to-store)))) + (if mode-line-update-p + `(backquote (prog1 ,form (force-mode-line-update t))) + `(backquote ,form))))))) (eglot--define-process-var eglot--short-name nil - "A short name for the process") + "A short name for the process" t) (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") @@ -91,11 +99,11 @@ (eglot--define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. -A list (ID WHAT DONE-P).") +A list (ID WHAT DONE-P)." t) (eglot--define-process-var eglot--status `(:unknown nil) "Status as declared by the server. -A list (WHAT SERIOUS-P).") +A list (WHAT SERIOUS-P)." t) (defun eglot--command (&optional errorp) (let ((probe (cdr (assoc major-mode eglot-executables)))) From ef1924c8e2f562a828e1f6a0ac17058b8a5b50fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 13:31:26 +0100 Subject: [PATCH 026/771] Add eglot-clear-status interactive command * eglot.el (eglot-clear-status): New (eglot-forget-pending-continuations): Fix bug. (eglot--mode-line-format): Add link to eglot-clear-status. --- lisp/progmodes/eglot.el | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1a82cc507d3..db594f4984a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -361,9 +361,14 @@ identifier. ERROR is non-nil if this is an error." (defun eglot-forget-pending-continuations (process) "Stop waiting for responses from the current LSP PROCESS." - (interactive (eglot--current-process-or-lose)) + (interactive (list (eglot--current-process-or-lose))) (clrhash (eglot--pending-continuations process))) +(defun eglot-clear-status (process) + "Clear most recent error message from PROCESS." + (interactive (list (eglot--current-process-or-lose))) + (setf (eglot--status process) nil)) + (cl-defun eglot--request (process method params @@ -668,12 +673,15 @@ running. INTERACTIVE is t if called interactively." `("/" (:propertize ,status - help-echo ,(concat "mouse-1: go to events buffer") + help-echo ,(concat "mouse-1: go to events buffer\n" + "mouse-3: clear this status") mouse-face mode-line-highlight face compilation-mode-line-fail keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] 'eglot-events-buffer) + (define-key map [mode-line mouse-3] + 'eglot-clear-status) map)))) ,@(when (and doing (not done-p)) `("/" From 4a87a536b97fcf00d28f6603132c6f960dd5f80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 13:32:13 +0100 Subject: [PATCH 027/771] Correctly report what we currently are capable of Which is almost nothing. * eglot.el (eglot--protocol-initialize): Clean up. --- lisp/progmodes/eglot.el | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index db594f4984a..1f2737effd6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -459,20 +459,23 @@ INTERACTIVE is t if caller was called interactively." (eglot--request process :initialize - `(:processId ,(emacs-pid) - :rootPath ,(concat - (expand-file-name (car (project-roots - (project-current))))) - :initializationOptions [] - :capabilities (:workspace (:executeCommand (:dynamicRegistration t)) - :textDocument (:synchronization (:didSave t)))) + (eglot--obj :processId (emacs-pid) + :rootPath (concat + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities + (eglot--obj + :workspace (eglot--obj) + :textDocument (eglot--obj + :publishDiagnostics `(:relatedInformation t)))) :success-fn (cl-function (lambda (&key capabilities) (setf (eglot--capabilities process) capabilities) (when interactive (setf (eglot--status process) nil) (eglot--message - "So yeah I got lots (%d) of capabilities" + "Server reports %d capabilities" (length capabilities))))))) (defun eglot-quit-server (process &optional sync interactive) From 9bd7605d12c1a842dc6806e4199e64d8e2f89a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 13:33:02 +0100 Subject: [PATCH 028/771] Change status to error everytime an error is found * eglot.el (eglot--process-receive): Also set error status. (eglot--request): Fix a compilation warning. --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1f2737effd6..457f3168938 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -317,6 +317,8 @@ identifier. ERROR is non-nil if this is an error." message response-id err) + (when err + (setf (eglot--status proc) '("error" t))) (cond ((and response-id (not continuations)) (eglot--warn "Ooops no continuation for id %s" response-id)) @@ -384,7 +386,7 @@ identifier. ERROR is non-nil if this is an error." (error-fn (or error-fn (cl-function - (lambda (&key data code message &allow-other-keys) + (lambda (&key code message &allow-other-keys) (setf (eglot--status process) '("error" t)) (eglot--warn "(request) Request id=%s errored with code=%s: %s" From 5ece72dc5cc272b60458f3166cfeb31bfa55a3f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 13:37:35 +0100 Subject: [PATCH 029/771] Events buffer uses eglot-mode, source buffers use eglot-editing-mode * eglot.el (eglot--special-buffer-process): New var. (eglot--current-process): Consider eglot--special-buffer-process. (eglot-events-buffer): Use eglot-mode (eglot-editing-mode): New minor mode. (eglot-mode): Turns on eglot-editing-mode maybe. --- lisp/progmodes/eglot.el | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 457f3168938..9742f885d9c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -43,11 +43,15 @@ (defvar eglot--processes-by-project (make-hash-table :test #'equal)) +(defvar eglot--special-buffer-process nil + "Current buffer's eglot process.") + (defun eglot--current-process () "The current logical EGLOT process." - (let ((cur (project-current))) - (and cur - (gethash cur eglot--processes-by-project)))) + (or eglot--special-buffer-process + (let ((cur (project-current))) + (and cur + (gethash cur eglot--processes-by-project))))) (defun eglot--current-process-or-lose () (or (eglot--current-process) @@ -270,8 +274,9 @@ INTERACTIVE is t if called interactively." (with-current-buffer buffer (buffer-disable-undo) (read-only-mode t) - (setf (eglot--events-buffer process) - buffer)) + (setf (eglot--events-buffer process) buffer + eglot--special-buffer-process process) + (eglot-mode)) buffer)))) (when interactive (display-buffer buffer)) @@ -610,12 +615,15 @@ running. INTERACTIVE is t if called interactively." (defvar eglot-mode-map (make-sparse-keymap)) -(define-minor-mode eglot-mode - "Minor mode for buffers where EGLOT is possible" +(defvar eglot-editing-mode-map (make-sparse-keymap)) + +(define-minor-mode eglot-editing-mode + "Minor mode for source buffers where EGLOT helps you edit." nil nil eglot-mode-map - (cond (eglot-mode + (cond (eglot-editing-mode + (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (if (eglot--current-process) @@ -625,6 +633,17 @@ running. INTERACTIVE is t if called interactively." (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t)))) +(define-minor-mode eglot-mode + "Minor mode for all buffers managed by EGLOT in some way." nil + nil eglot-mode-map + (cond (eglot-mode + (when (and buffer-file-name + (not eglot-editing-mode)) + (eglot-editing-mode 1))) + (t + (when eglot-editing-mode + (eglot-editing-mode -1))))) + (defvar eglot-menu) (easy-menu-define eglot-menu eglot-mode-map "EGLOT" From 4602fc02aefa84339b4be3a7fe23d44a4a4af00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 13:54:37 +0100 Subject: [PATCH 030/771] Less obstrusive flymake stuff for now * eglot.el (eglot--after-change, eglot-flymake-backend): Fix. --- lisp/progmodes/eglot.el | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9742f885d9c..fe63a29bc4c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -775,7 +775,8 @@ running. INTERACTIVE is t if called interactively." (defun eglot--after-change (start end length) (cl-incf eglot--versioned-identifier) (push (list start end length) eglot--recent-changes) - (eglot--message "start is %s, end is %s, length is %s" start end length)) + ;; (eglot--message "start is %s, end is %s, length is %s" start end length) + ) (defun eglot--signalDidOpen () (eglot--notify (eglot--current-process-or-lose) @@ -816,6 +817,9 @@ running. INTERACTIVE is t if called interactively." (setq eglot--recent-changes nil))) (defun eglot-flymake-backend (report-fn &rest _more) + "An EGLOT Flymake backend. +Calls REPORT-FN maybe if server publishes diagnostics in time." + ;; FIXME: perhaps should call it immediately? (setq eglot--current-flymake-report-fn report-fn) (eglot--maybe-signal-didChange)) From dd467a4706c693dd8a66392d69e74100497ddb04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 15:06:50 +0100 Subject: [PATCH 031/771] Rework commands for connecting and reconnecting * eglot.el (eglot--current-process-or-lose): Add doc. (eglot--command): Remove. (eglot--bootstrap-fn): New process-local variable. (eglot--connect): Redesign. (eglot-make-local-process): New function. (eglot-reconnect): New interactive command. (eglot-new-process): Redesign. (eglot--process-sentinel): Add doc. (eglot--protocol-initialize): Rework. (eglot--mode-line-format): Use eglot-reconnect. --- lisp/progmodes/eglot.el | 139 +++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 58 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fe63a29bc4c..c7bb5d46f32 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -54,6 +54,7 @@ (gethash cur eglot--processes-by-project))))) (defun eglot--current-process-or-lose () + "Return the current EGLOT process or error." (or (eglot--current-process) (eglot--error "No current EGLOT process%s" (if (project-current) "" @@ -109,29 +110,62 @@ A list (ID WHAT DONE-P)." t) "Status as declared by the server. A list (WHAT SERIOUS-P)." t) -(defun eglot--command (&optional errorp) - (let ((probe (cdr (assoc major-mode eglot-executables)))) - (unless (or (not errorp) - probe) - (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)) - probe)) +(eglot--define-process-var eglot--bootstrap-fn nil + "Function for returning processes/connetions to LSP servers. +Must be a function of one arg, a name, returning a process +object.") -(defun eglot--connect (name filter sentinel) - "Helper for `eglot-new-process'. +(defun eglot-make-local-process (name command) + "Make a local LSP process from COMMAND. NAME is a name to give the inferior process or connection. -FILTER and SENTINEL are filter and sentinel. -Should return a list of (PROCESS BUFFER)." - (let ((proc (make-process :name name - :buffer (get-buffer-create - (format "*%s inferior*" name)) - :command (eglot--command 'error) - :connection-type 'pipe - :filter filter - :sentinel sentinel - :stderr (get-buffer-create (format "*%s stderr*" - name))))) - (list proc (process-buffer proc)))) +Returns a process object." + (let* ((readable-name (format "EGLOT server (%s)" name)) + (proc + (make-process + :name readable-name + :buffer (get-buffer-create + (format "*%s inferior*" readable-name)) + :command command + :connection-type 'pipe + :filter 'eglot--process-filter + :sentinel 'eglot--process-sentinel + :stderr (get-buffer-create (format "*%s stderr*" + name))))) + proc)) + +(defun eglot--connect (short-name bootstrap-fn &optional success-fn) + "Make a connection with SHORT-NAME and BOOTSTRAP-FN. +Call SUCCESS-FN with no args if all goes well." + (let* ((proc (funcall bootstrap-fn short-name)) + (buffer (process-buffer proc))) + (setf (eglot--bootstrap-fn proc) bootstrap-fn) + (with-current-buffer buffer + (let ((inhibit-read-only t)) + (setf (eglot--short-name proc) short-name) + (puthash (project-current) proc eglot--processes-by-project) + (erase-buffer) + (read-only-mode t) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (insert + (format "\n-----------------------------------\n")))) + (eglot--protocol-initialize + proc + (cl-function + (lambda (&key capabilities) + (setf (eglot--capabilities proc) capabilities) + (setf (eglot--status proc) nil) + (when success-fn (funcall success-fn))))))))) + +(defun eglot-reconnect (process &optional interactive) + "Reconnect to PROCESS. +INTERACTIVE is t if called interactively." + (interactive (list (eglot--current-process-or-lose) t)) + (eglot-quit-server process 'sync interactive) + (eglot--connect (eglot--short-name process) + (eglot--bootstrap-fn process) + (lambda () + (eglot--message "Reconnected")))) (defun eglot-new-process (&optional interactive) "Start a new EGLOT process and initialize it. @@ -140,32 +174,28 @@ INTERACTIVE is t if called interactively." (let ((project (project-current))) (unless project (eglot--error "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process))) - (when (and current-process - (process-live-p current-process)) - (eglot--message "(new-process) Asking current process to terminate first") - (eglot-quit-server current-process 'sync interactive))) - (let* ((short-name (file-name-base - (directory-file-name - (car (project-roots (project-current)))))) - (good-name - (format "EGLOT server (%s)" short-name))) - (pcase-let ((`(,proc ,buffer) - (eglot--connect good-name - 'eglot--process-filter - 'eglot--process-sentinel))) - (with-current-buffer buffer - (let ((inhibit-read-only t)) - (setf (eglot--short-name proc) short-name) - (puthash (project-current) proc eglot--processes-by-project) - (erase-buffer) - (read-only-mode t) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert - (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize proc interactive))))))) + (cond ((and current-process + (process-live-p current-process)) + (eglot--message "(new-process) Reconnecting instead") + (eglot-reconnect current-process interactive)) + (t + (eglot--connect + (file-name-base + (directory-file-name + (car (project-roots (project-current))))) + (lambda (name) + (eglot-make-local-process + name + (let ((probe (cdr (assoc major-mode eglot-executables)))) + (unless probe + (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)) + probe))) + (lambda () + (eglot--message "Connected")))))))) (defun eglot--process-sentinel (process change) + "Called with PROCESS undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) (when (not (process-live-p process)) ;; Remember to cancel all timers @@ -459,10 +489,10 @@ identifier. ERROR is non-nil if this is an error." ;;; Requests ;;; -(defun eglot--protocol-initialize (process interactive) +(defun eglot--protocol-initialize (process success-fn) "Initialize LSP protocol. -PROCESS is a connected process (network or local). -INTERACTIVE is t if caller was called interactively." +PROCESS is a connected process (network or local). SUCCESS-FN is +called with capabilites after connection." (eglot--request process :initialize @@ -476,14 +506,7 @@ INTERACTIVE is t if caller was called interactively." :workspace (eglot--obj) :textDocument (eglot--obj :publishDiagnostics `(:relatedInformation t)))) - :success-fn (cl-function - (lambda (&key capabilities) - (setf (eglot--capabilities process) capabilities) - (when interactive - (setf (eglot--status process) nil) - (eglot--message - "Server reports %d capabilities" - (length capabilities))))))) + :success-fn success-fn)) (defun eglot-quit-server (process &optional sync interactive) "Politely ask the server PROCESS to quit. @@ -687,12 +710,12 @@ running. INTERACTIVE is t if called interactively." keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] 'eglot-events-buffer) (define-key map [mode-line mouse-2] 'eglot-quit-server) - (define-key map [mode-line mouse-3] 'eglot-new-process) + (define-key map [mode-line mouse-3] 'eglot-reconnect) map) mouse-face mode-line-highlight help-echo ,(concat "mouse-1: go to events buffer\n" "mouse-2: quit server\n" - "mouse-3: new process")) + "mouse-3: reconnect to server")) ,@(when serious-p `("/" (:propertize From f35d1d51cd9bcf508607ea083c4c30c6818153dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 15:35:22 +0100 Subject: [PATCH 032/771] Don't clutter ui with warnings * eglot.el (warnings): require it. (eglot--warn): set warning-minimum-level --- lisp/progmodes/eglot.el | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c7bb5d46f32..6e5954ca807 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -31,6 +31,7 @@ (require 'url-util) (require 'pcase) (require 'compile) ; for some faces +(require 'warnings) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -623,9 +624,10 @@ running. INTERACTIVE is t if called interactively." (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." - (display-warning 'eglot - (apply #'format format args) - :warning)) + (let ((warning-minimum-level :error)) + (display-warning 'eglot + (apply #'format format args) + :warning))) From bdfba7ed6290554e2908e1aaba2f5887bc073c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 15:36:26 +0100 Subject: [PATCH 033/771] Slightly more user friendly start * eglot.el (eglot-new-process): signal DidOpen for every file in project. (eglot-editing-mode): Offer to start process. --- lisp/progmodes/eglot.el | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6e5954ca807..d3428a73a74 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -193,7 +193,17 @@ INTERACTIVE is t if called interactively." major-mode)) probe))) (lambda () - (eglot--message "Connected")))))))) + (eglot--message "Connected") + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (if (and buffer-file-name + (cl-some + (lambda (root) + (string-prefix-p + (expand-file-name root) + (expand-file-name buffer-file-name))) + (project-roots project))) + (eglot--signalDidOpen))))))))))) (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." @@ -647,16 +657,19 @@ running. INTERACTIVE is t if called interactively." nil nil eglot-mode-map - (cond (eglot-editing-mode - (eglot-mode 1) - (add-hook 'after-change-functions 'eglot--after-change nil t) - (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) - (if (eglot--current-process) - (eglot--signalDidOpen) - (eglot--warn "No process"))) - (t - (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) - (remove-hook 'after-change-functions 'eglot--after-change t)))) + (cond + (eglot-editing-mode + (eglot-mode 1) + (add-hook 'after-change-functions 'eglot--after-change nil t) + (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) + (if (eglot--current-process) + (eglot--signalDidOpen) + (if (y-or-n-p "No process, try to start one with `eglot-new-process'? ") + (eglot-new-process t) + (eglot--warn "No process")))) + (t + (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) + (remove-hook 'after-change-functions 'eglot--after-change t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil From 2b8142d285a0a545abf4cbbe108443b861c50959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 15:49:41 +0100 Subject: [PATCH 034/771] Ready to start fixing flymake integration * eglot.el (eglot-editing-mode): Turn on flymake-mode. (eglot-flymake-backend): Always start by reporting no diagnostics. (eglot--textDocument/publishDiagnostics): No annoying message. --- lisp/progmodes/eglot.el | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d3428a73a74..aa23252271c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -573,7 +573,6 @@ running. INTERACTIVE is t if called interactively." (eglot--warn "outdated publishDiagnostics report from server")) (buffer (with-current-buffer buffer - (eglot--message "OK so add some %s diags" (length diagnostics)) (cl-flet ((pos-at (pos-plist) (car (flymake-diag-region @@ -662,6 +661,7 @@ running. INTERACTIVE is t if called interactively." (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) + (flymake-mode 1) (if (eglot--current-process) (eglot--signalDidOpen) (if (y-or-n-p "No process, try to start one with `eglot-new-process'? ") @@ -857,7 +857,10 @@ running. INTERACTIVE is t if called interactively." (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." - ;; FIXME: perhaps should call it immediately? + ;; call immediately with no diagnostics, this just means we don't + ;; have them yet (and also clears any pending ones). + ;; + (funcall report-fn nil) (setq eglot--current-flymake-report-fn report-fn) (eglot--maybe-signal-didChange)) From dfe551f5776498c14691ef4e820ad2342a90e731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 May 2018 17:00:46 +0100 Subject: [PATCH 035/771] Auto-reconnect on unexpected connection loss * eglot.el (eglot-reconnect): Only quit if indeed not quit already. (eglot-new-process): Burn the command in the bootstrap fn. (eglot--process-sentinel): Automatically reconnect if closed unexpectedly. (eglot--warn): Also message to *Messages* --- lisp/progmodes/eglot.el | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aa23252271c..71dab80a41b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -162,7 +162,8 @@ Call SUCCESS-FN with no args if all goes well." "Reconnect to PROCESS. INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) - (eglot-quit-server process 'sync interactive) + (when (process-live-p process) + (eglot-quit-server process 'sync interactive)) (eglot--connect (eglot--short-name process) (eglot--bootstrap-fn process) (lambda () @@ -174,7 +175,12 @@ INTERACTIVE is t if called interactively." (interactive (list t)) (let ((project (project-current))) (unless project (eglot--error "(new-process) Cannot work without a current project!")) - (let ((current-process (eglot--current-process))) + (let ((current-process (eglot--current-process)) + (command (let ((probe (cdr (assoc major-mode eglot-executables)))) + (unless probe + (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)) + probe))) (cond ((and current-process (process-live-p current-process)) (eglot--message "(new-process) Reconnecting instead") @@ -187,11 +193,7 @@ INTERACTIVE is t if called interactively." (lambda (name) (eglot-make-local-process name - (let ((probe (cdr (assoc major-mode eglot-executables)))) - (unless probe - (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)) - probe))) + command)) (lambda () (eglot--message "Connected") (dolist (buffer (buffer-list)) @@ -221,8 +223,10 @@ INTERACTIVE is t if called interactively." (eglot--message "(sentinel) Moribund process exited with status %s" (process-exit-status process))) (t - (eglot--warn "(sentinel) Process unexpectedly changed to %s" - change))) + (eglot--warn + "(sentinel) Reconnecting after process unexpectedly changed to %s." + change) + (eglot-reconnect process))) (delete-process process))) (defun eglot--process-filter (proc string) @@ -633,6 +637,7 @@ running. INTERACTIVE is t if called interactively." (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." + (apply #'eglot--message (concat "(warning) " format) args) (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) From b81dcb530f98dc04b734666c72cca53d8d1127d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 11:39:49 +0100 Subject: [PATCH 036/771] Redesign and simplify parser Fix horrible bugs. This is the correct way. * eglot.el (eglot--process-filter): Redesign. --- lisp/progmodes/eglot.el | 106 ++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 59 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 71dab80a41b..b9aa94c322a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -234,71 +234,59 @@ INTERACTIVE is t if called interactively." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) (let ((inhibit-read-only t) - (pre-insertion-mark (copy-marker (process-mark proc))) (expected-bytes (eglot--expected-bytes proc))) ;; Insert the text, advancing the process marker. - (goto-char (process-mark proc)) - (insert string) - (set-marker (process-mark proc) (point)) - - ;; goto point just before insertion ;; - (goto-char pre-insertion-mark) - - ;; loop for each message (more than one might have arrived) + (save-excursion + (goto-char (process-mark proc)) + (insert string) + (set-marker (process-mark proc) (point))) + ;; Loop (more than one message might have arrived) ;; (catch 'done (while t - (let* ((match (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t)) - (new-expected-bytes (and match - (string-to-number (match-string 1))))) - (when new-expected-bytes - (when expected-bytes - (eglot--warn - (concat "Unexpectedly starting new message but %s bytes " - "reportedly remaining from previous one") - expected-bytes)) - (setf (eglot--expected-bytes proc) new-expected-bytes) - (setq expected-bytes new-expected-bytes))) - - ;; check for message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond ((not expected-bytes) ; previous search didn't match - (eglot--warn - "Skipping %s bytes of unexpected garbage from process %s" - available-bytes - proc) - (goto-char (process-mark proc)) - (throw 'done :skipping-garbage)) - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) - message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; process in another buffer, shielding - ;; buffer from tamper - (with-temp-buffer - (eglot--process-receive proc json-message)))) - (goto-char message-end) - (setf (eglot--expected-bytes proc) nil - expected-bytes nil))) - (when (= (point) (process-mark proc)) - (throw 'done :clean-done))) - (t - ;; just adding some stuff to the end that doesn't yet - ;; complete the message - (throw 'done :waiting-for-more-bytes)))))))))) + (cond ((not expected-bytes) + ;; Starting a new message + ;; + (setq expected-bytes + (and (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" + (+ (point) 100) + t) + (string-to-number (match-string 1)))) + (unless expected-bytes + (throw 'done :waiting-for-new-message))) + (t + ;; Attempt to complete a message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; Process content in another buffer, + ;; shielding buffer from tamper + ;; + (with-temp-buffer + (eglot--process-receive proc json-message)))) + (goto-char message-end) + (delete-region (point-min) (point)) + (setq expected-bytes nil)))) + (t + ;; Message is still incomplete + ;; + (throw 'done :waiting-for-more-bytes-in-this-message)))))))) + ;; Saved parsing state for next visit to this filter + ;; + (setf (eglot--expected-bytes proc) expected-bytes))))) (defmacro eglot--obj (&rest what) "Make WHAT a suitable argument for `json-encode'." From 8277231fa82e8a6cc92f9bac6a6d4d1e07ea7197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 11:48:35 +0100 Subject: [PATCH 037/771] Rework connection restarting again Quitting a process removes it from the project. * eglot.el (eglot-editing-mode,eglot-mode): Forward declare. (eglot--project): New process-local var. (eglot--connect): Takes a project. (eglot-new-process): Rework. (eglot--sentinel): Remove proc from eglot--processes-by-project. --- lisp/progmodes/eglot.el | 85 ++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b9aa94c322a..12e7fef08c4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -44,6 +44,9 @@ (defvar eglot--processes-by-project (make-hash-table :test #'equal)) +(defvar eglot-editing-mode) ; forward decl +(defvar eglot-mode) ; forward decl + (defvar eglot--special-buffer-process nil "Current buffer's eglot process.") @@ -103,6 +106,9 @@ after setting it." (eglot--define-process-var eglot--moribund nil "Non-nil if process is about to exit") +(eglot--define-process-var eglot--project nil + "The project the process belongs to.") + (eglot--define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. A list (ID WHAT DONE-P)." t) @@ -134,12 +140,13 @@ Returns a process object." name))))) proc)) -(defun eglot--connect (short-name bootstrap-fn &optional success-fn) - "Make a connection with SHORT-NAME and BOOTSTRAP-FN. +(defun eglot--connect (project short-name bootstrap-fn &optional success-fn) + "Make a connection with PROJECT, SHORT-NAME and BOOTSTRAP-FN. Call SUCCESS-FN with no args if all goes well." (let* ((proc (funcall bootstrap-fn short-name)) (buffer (process-buffer proc))) - (setf (eglot--bootstrap-fn proc) bootstrap-fn) + (setf (eglot--bootstrap-fn proc) bootstrap-fn + (eglot--project proc) project) (with-current-buffer buffer (let ((inhibit-read-only t)) (setf (eglot--short-name proc) short-name) @@ -164,48 +171,57 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) (when (process-live-p process) (eglot-quit-server process 'sync interactive)) - (eglot--connect (eglot--short-name process) - (eglot--bootstrap-fn process) - (lambda () - (eglot--message "Reconnected")))) + (eglot--connect + (eglot--project process) + (eglot--short-name process) + (eglot--bootstrap-fn process) + (lambda () + (eglot--message "Reconnected")))) (defun eglot-new-process (&optional interactive) "Start a new EGLOT process and initialize it. INTERACTIVE is t if called interactively." (interactive (list t)) (let ((project (project-current))) - (unless project (eglot--error "(new-process) Cannot work without a current project!")) + (unless project (eglot--error + "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process)) (command (let ((probe (cdr (assoc major-mode eglot-executables)))) (unless probe (eglot--error "Don't know how to start EGLOT for %s buffers" major-mode)) probe))) - (cond ((and current-process - (process-live-p current-process)) - (eglot--message "(new-process) Reconnecting instead") - (eglot-reconnect current-process interactive)) - (t - (eglot--connect - (file-name-base - (directory-file-name - (car (project-roots (project-current))))) - (lambda (name) - (eglot-make-local-process - name - command)) - (lambda () - (eglot--message "Connected") - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (if (and buffer-file-name - (cl-some - (lambda (root) - (string-prefix-p - (expand-file-name root) - (expand-file-name buffer-file-name))) - (project-roots project))) - (eglot--signalDidOpen))))))))))) + (cond + ((and current-process + (process-live-p current-process)) + (when (and + interactive + (y-or-n-p "[eglot] Live process found, reconnect instead? ")) + (eglot-reconnect current-process interactive))) + (t + (eglot--connect + project + (file-name-base + (directory-file-name + (car (project-roots (project-current))))) + (lambda (name) + (eglot-make-local-process + name + command)) + (lambda () + (eglot--message "Connected") + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when(and buffer-file-name + (cl-some + (lambda (root) + (string-prefix-p + (expand-file-name root) + (expand-file-name buffer-file-name))) + (project-roots project))) + (unless eglot-editing-mode + (eglot-editing-mode 1)) + (eglot--signalDidOpen))))))))))) (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." @@ -221,7 +237,8 @@ INTERACTIVE is t if called interactively." (eglot--pending-continuations process)) (cond ((eglot--moribund process) (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status process))) + (process-exit-status process)) + (remhash (eglot--project process) eglot--processes-by-project)) (t (eglot--warn "(sentinel) Reconnecting after process unexpectedly changed to %s." From 976896f2c34034a495c72096daeadeb2a9ab4846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 11:49:24 +0100 Subject: [PATCH 038/771] Signal textdocument/didclose * eglot.el (eglot-editing-mode): Signal didClose. (eglot--signalDidClose): New. --- lisp/progmodes/eglot.el | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 12e7fef08c4..85844409f34 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -671,6 +671,7 @@ running. INTERACTIVE is t if called interactively." (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) + (add-hook 'kill-buffer-hook 'eglot--signalDidClose nil t) (flymake-mode 1) (if (eglot--current-process) (eglot--signalDidOpen) @@ -679,7 +680,8 @@ running. INTERACTIVE is t if called interactively." (eglot--warn "No process")))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) - (remove-hook 'after-change-functions 'eglot--after-change t)))) + (remove-hook 'after-change-functions 'eglot--after-change t) + (remove-hook 'kill-buffer-hook 'eglot--signalDidClose t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -832,6 +834,12 @@ running. INTERACTIVE is t if called interactively." (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) +(defun eglot--signalDidClose () + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didClose + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) + (defun eglot--maybe-signal-didChange () (when eglot--recent-changes (save-excursion From a371a8d2ad0457c22e77205af4cd034e0620bf68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 12:05:37 +0100 Subject: [PATCH 039/771] Simplify flymake integration And get rid of the ridiculous environment thingy * eglot.el (eglot--process-sentinel): Continuations are triplets. (eglot--environment-vars, eglot--environment): Remove. (eglot--process-receive): Simplify. (eglot--unreported-diagnostics): New variable. (eglot--textDocument/publishDiagnostics): Simplify. (eglot-flymake-backend): Report unreported diagnostics. --- lisp/progmodes/eglot.el | 58 ++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 85844409f34..8bb0729d522 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -229,8 +229,8 @@ INTERACTIVE is t if called interactively." (when (not (process-live-p process)) ;; Remember to cancel all timers ;; - (maphash (lambda (id quad) - (cl-destructuring-bind (_success _error timeout _env) quad + (maphash (lambda (id triplet) + (cl-destructuring-bind (_success _error timeout) triplet (eglot--message "(sentinel) Cancelling timer for continuation %s" id) (cancel-timer timeout))) @@ -349,13 +349,6 @@ identifier. ERROR is non-nil if this is an error." (setq msg (propertize msg 'face 'error))) (insert msg))))) -(defvar eglot--environment-vars - '(eglot--current-flymake-report-fn) - "A list of variables with saved values on every request.") - -(defvar eglot--environment nil - "Dynamically bound alist of symbol and values.") - (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." (let* ((response-id (plist-get message :id)) @@ -388,13 +381,9 @@ identifier. ERROR is non-nil if this is an error." (t (let* ((method (plist-get message :method)) (handler-sym (intern (concat "eglot--" - method))) - (eglot--environment (cl-fourth continuations))) + method)))) (if (functionp handler-sym) - (cl-progv - (mapcar #'car eglot--environment) - (mapcar #'cdr eglot--environment) - (apply handler-sym proc (plist-get message :params))) + (apply handler-sym proc (plist-get message :params)) (eglot--debug "No implemetation for notification %s yet" method))))))) @@ -476,9 +465,7 @@ identifier. ERROR is non-nil if this is an error." error-fn (lambda (&rest args) (throw catch-tag (apply error-fn args)))) - timeout-timer - (cl-loop for var in eglot--environment-vars - collect (cons var (symbol-value var)))) + timeout-timer) (eglot--pending-continuations process)) (unless async-p (unwind-protect @@ -563,23 +550,18 @@ running. INTERACTIVE is t if called interactively." ;;; Notifications ;;; -(defvar eglot--current-flymake-report-fn nil) +(defvar-local eglot--current-flymake-report-fn nil + "Current flymake report function for this buffer") +(defvar-local eglot--unreported-diagnostics nil + "Unreported diagnostics for this buffer.") (cl-defun eglot--textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" (let* ((obj (url-generic-parse-url uri)) (filename (car (url-path-and-query obj))) - (buffer (find-buffer-visiting filename)) - (report-fn (cdr (assoc 'eglot--current-flymake-report-fn - eglot--environment)))) + (buffer (find-buffer-visiting filename))) (cond - ((not eglot--current-flymake-report-fn) - (eglot--warn "publishDiagnostics called but no report-fn")) - ((and report-fn - (not (eq report-fn - eglot--current-flymake-report-fn))) - (eglot--warn "outdated publishDiagnostics report from server")) (buffer (with-current-buffer buffer (cl-flet ((pos-at @@ -607,9 +589,12 @@ running. INTERACTIVE is t if called interactively." :note)) message)))) into diags - finally (funcall - eglot--current-flymake-report-fn - diags))))) + finally + (if eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics + diags)))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -875,11 +860,14 @@ running. INTERACTIVE is t if called interactively." (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." - ;; call immediately with no diagnostics, this just means we don't - ;; have them yet (and also clears any pending ones). - ;; - (funcall report-fn nil) + ;; Call immediately with anything unreported (this will clear any + ;; pending diags) + (funcall report-fn eglot--unreported-diagnostics) + (setq eglot--unreported-diagnostics nil) + ;; Setup so maybe it's called later, too. (setq eglot--current-flymake-report-fn report-fn) + ;; Take this opportunity to signal a didChange that might eventually + ;; make the server report new diagnostics. (eglot--maybe-signal-didChange)) From b950cb40b63b57a647cd29eb8c78481b848d77e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 13:32:20 +0100 Subject: [PATCH 040/771] Appease checkdoc.el * eglot.el (eglot--process-send, eglot--next-request-id) (eglot--current-buffer-VersionedTextDocumentIdentifier) (eglot--current-buffer-TextDocumentItem) (eglot--after-change, eglot--signalDidOpen) (eglot--signalDidClose, eglot--maybe-signal-didChange): Add docstring. --- lisp/progmodes/eglot.el | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8bb0729d522..cd546a32d65 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -390,6 +390,7 @@ identifier. ERROR is non-nil if this is an error." (defvar eglot--expect-carriage-return nil) (defun eglot--process-send (id proc message) + "Send MESSAGE to PROC (ID is optional)." (let* ((json (json-encode message)) (to-send (format "Content-Length: %d\r\n\r\n%s" (string-bytes json) @@ -403,6 +404,7 @@ identifier. ERROR is non-nil if this is an error." (defvar eglot--next-request-id 0) (defun eglot--next-request-id () + "Compute the next id for a client request." (setq eglot--next-request-id (1+ eglot--next-request-id))) (defun eglot-forget-pending-continuations (process) @@ -789,6 +791,7 @@ running. INTERACTIVE is t if called interactively." eglot--versioned-identifier) (defun eglot--current-buffer-VersionedTextDocumentIdentifier () + "Compute VersionedTextDocumentIdentifier object for current buffer." (eglot--obj :uri (concat "file://" (url-hexify-string @@ -797,6 +800,7 @@ running. INTERACTIVE is t if called interactively." :version (eglot--current-buffer-versioned-identifier))) (defun eglot--current-buffer-TextDocumentItem () + "Compute TextDocumentItem object for current buffer." (append (eglot--current-buffer-VersionedTextDocumentIdentifier) (eglot--obj :languageId (cdr (assoc major-mode @@ -808,24 +812,29 @@ running. INTERACTIVE is t if called interactively." (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--after-change (start end length) + "Hook onto `after-change-functions'. +Records START, END and LENGTH locally." (cl-incf eglot--versioned-identifier) (push (list start end length) eglot--recent-changes) ;; (eglot--message "start is %s, end is %s, length is %s" start end length) ) (defun eglot--signalDidOpen () + "Send textDocument/didOpen to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) (defun eglot--signalDidClose () + "Send textDocument/didClose to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didClose (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) (defun eglot--maybe-signal-didChange () + "Send textDocument/didChange to server." (when eglot--recent-changes (save-excursion (save-restriction From 508c8efe23b26e585deefb897586257ab9bc14ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 13:37:04 +0100 Subject: [PATCH 041/771] Multiple servers per project are possible A server manages a specific major-mode within a project. * eglot.el (eglot--processes-by-project): Add docstring. (eglot--current-process): Search new eglot--processes-by-project format. (eglot--major-mode): New variable. (eglot--moribund, eglot--project): Update docstring. (eglot--project-short-name, eglot--all-major-modes): New helpers. (eglot--connect): Rework. (eglot-new-process): Rework severely. (eglot--command-history): New variable. (eglot--process-sentinel): Use new eglot--processes-by-project. Update mode line. (eglot-editing-mode): Don't start processes, just suggest it. --- lisp/progmodes/eglot.el | 123 +++++++++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 28 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd546a32d65..10af6c58769 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -42,7 +42,8 @@ (python-mode . ("pyls"))) "Alist mapping major modes to server executables.") -(defvar eglot--processes-by-project (make-hash-table :test #'equal)) +(defvar eglot--processes-by-project (make-hash-table :test #'equal) + "Keys are projects. Values are lists of processes.") (defvar eglot-editing-mode) ; forward decl (defvar eglot-mode) ; forward decl @@ -53,9 +54,13 @@ (defun eglot--current-process () "The current logical EGLOT process." (or eglot--special-buffer-process - (let ((cur (project-current))) - (and cur - (gethash cur eglot--processes-by-project))))) + (let* ((cur (project-current)) + (processes + (and cur + (gethash cur eglot--processes-by-project)))) + (cl-find major-mode + processes + :key #'eglot--major-mode)))) (defun eglot--current-process-or-lose () "Return the current EGLOT process or error." @@ -91,6 +96,9 @@ after setting it." (eglot--define-process-var eglot--short-name nil "A short name for the process" t) +(eglot--define-process-var eglot--major-mode nil + "The major-mode this server is managing.") + (eglot--define-process-var eglot--expected-bytes nil "How many bytes declared by server") @@ -104,10 +112,10 @@ after setting it." "Holds list of capabilities that server reported") (eglot--define-process-var eglot--moribund nil - "Non-nil if process is about to exit") + "Non-nil if server is about to exit") (eglot--define-process-var eglot--project nil - "The project the process belongs to.") + "The project the server belongs to.") (eglot--define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. @@ -122,6 +130,20 @@ A list (WHAT SERIOUS-P)." t) Must be a function of one arg, a name, returning a process object.") +(defun eglot--project-short-name (project) + "Give PROJECT a short name." + (file-name-base + (directory-file-name + (car (project-roots project))))) + +(defun eglot--all-major-modes () + "Return all know major modes." + (let ((retval)) + (mapatoms (lambda (sym) + (when (plist-member (symbol-plist sym) 'derived-mode-parent) + (push sym retval)))) + retval)) + (defun eglot-make-local-process (name command) "Make a local LSP process from COMMAND. NAME is a name to give the inferior process or connection. @@ -140,17 +162,22 @@ Returns a process object." name))))) proc)) -(defun eglot--connect (project short-name bootstrap-fn &optional success-fn) - "Make a connection with PROJECT, SHORT-NAME and BOOTSTRAP-FN. -Call SUCCESS-FN with no args if all goes well." +(defun eglot--connect (project managed-major-mode + short-name bootstrap-fn &optional success-fn) + "Make a connection for PROJECT, SHORT-NAME and MANAGED-MAJOR-MODE. +Use BOOTSTRAP-FN to make the actual process object. Call +SUCCESS-FN with no args if all goes well." (let* ((proc (funcall bootstrap-fn short-name)) (buffer (process-buffer proc))) (setf (eglot--bootstrap-fn proc) bootstrap-fn - (eglot--project proc) project) + (eglot--project proc) project + (eglot--major-mode proc) managed-major-mode) (with-current-buffer buffer (let ((inhibit-read-only t)) (setf (eglot--short-name proc) short-name) - (puthash (project-current) proc eglot--processes-by-project) + (push proc + (gethash (project-current) + eglot--processes-by-project)) (erase-buffer) (read-only-mode t) (with-current-buffer (eglot-events-buffer proc) @@ -173,24 +200,63 @@ INTERACTIVE is t if called interactively." (eglot-quit-server process 'sync interactive)) (eglot--connect (eglot--project process) + (eglot--major-mode process) (eglot--short-name process) (eglot--bootstrap-fn process) (lambda () (eglot--message "Reconnected")))) -(defun eglot-new-process (&optional interactive) - "Start a new EGLOT process and initialize it. +(defvar eglot--command-history nil + "History of COMMAND arguments to `eglot-new-process'.") + +(defun eglot-new-process (managed-major-mode command &optional interactive) + ;; FIXME: Later make this function also connect to TCP servers by + ;; overloading semantics on COMMAND. + "Start a Language Server Protocol server. +Server is started with COMMAND and manages buffers of +MANAGED-MAJOR-MODE for the current project. + +COMMAND is a list of strings, an executable program and +optionally its arguments. MANAGED-MAJOR-MODE is an Emacs major +mode. + +With a prefix arg, prompt for MANAGED-MAJOR-MODE and COMMAND, +else guess them from current context and `eglot-executables'. + INTERACTIVE is t if called interactively." - (interactive (list t)) - (let ((project (project-current))) + (interactive + (let* ((managed-major-mode + (cond + ((or current-prefix-arg + (not buffer-file-name)) + (intern + (completing-read + "[eglot] Start a server to manage buffers of what major mode? " + (mapcar #'symbol-name + (eglot--all-major-modes)) nil t + (symbol-name major-mode) nil + (symbol-name major-mode) nil))) + (t major-mode))) + (guessed-command + (cdr (assoc managed-major-mode eglot-executables)))) + (list + managed-major-mode + (if current-prefix-arg + (split-string-and-unquote + (read-shell-command "[eglot] Run program: " + (combine-and-quote-strings guessed-command) + 'eglot-command-history)) + guessed-command) + t))) + (let* ((project (project-current)) + (short-name (eglot--project-short-name project))) (unless project (eglot--error "(new-process) Cannot work without a current project!")) (let ((current-process (eglot--current-process)) - (command (let ((probe (cdr (assoc major-mode eglot-executables)))) - (unless probe - (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)) - probe))) + (command + (or command + (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)))) (cond ((and current-process (process-live-p current-process)) @@ -201,15 +267,15 @@ INTERACTIVE is t if called interactively." (t (eglot--connect project - (file-name-base - (directory-file-name - (car (project-roots (project-current))))) + managed-major-mode + short-name (lambda (name) (eglot-make-local-process name command)) (lambda () - (eglot--message "Connected") + (eglot--message "Connected. Managing `%s' buffers in project %s." + managed-major-mode short-name) (dolist (buffer (buffer-list)) (with-current-buffer buffer (when(and buffer-file-name @@ -238,12 +304,15 @@ INTERACTIVE is t if called interactively." (cond ((eglot--moribund process) (eglot--message "(sentinel) Moribund process exited with status %s" (process-exit-status process)) - (remhash (eglot--project process) eglot--processes-by-project)) + (setf (gethash (eglot--project process) eglot--processes-by-project) + (delq process + (gethash (eglot--project process) eglot--processes-by-project)))) (t (eglot--warn "(sentinel) Reconnecting after process unexpectedly changed to %s." change) (eglot-reconnect process))) + (force-mode-line-update t) (delete-process process))) (defun eglot--process-filter (proc string) @@ -662,9 +731,7 @@ running. INTERACTIVE is t if called interactively." (flymake-mode 1) (if (eglot--current-process) (eglot--signalDidOpen) - (if (y-or-n-p "No process, try to start one with `eglot-new-process'? ") - (eglot-new-process t) - (eglot--warn "No process")))) + (eglot--warn "No process, start one with `M-x eglot-new-process'"))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) From 7ba401ce38fc3f68eccffc547948ed432de8143e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 14:08:37 +0100 Subject: [PATCH 042/771] Watch for files opened under umbrella of existing process * eglot.el (eglot--connect): Call success-fn with a proc. (eglot-reconnect): Adapt to new eglot--connect. (eglot-new-process): Call eglot--maybe-activate-editing-mode (eglot--maybe-activate-editing-mode): New function. (find-file-hook): Add it here. --- lisp/progmodes/eglot.el | 42 ++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 10af6c58769..f4ac8584064 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -190,7 +190,7 @@ SUCCESS-FN with no args if all goes well." (lambda (&key capabilities) (setf (eglot--capabilities proc) capabilities) (setf (eglot--status proc) nil) - (when success-fn (funcall success-fn))))))))) + (when success-fn (funcall success-fn proc))))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -203,8 +203,8 @@ INTERACTIVE is t if called interactively." (eglot--major-mode process) (eglot--short-name process) (eglot--bootstrap-fn process) - (lambda () - (eglot--message "Reconnected")))) + (lambda (_proc) + (eglot--message "Reconnected!")))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot-new-process'.") @@ -273,21 +273,15 @@ INTERACTIVE is t if called interactively." (eglot-make-local-process name command)) - (lambda () - (eglot--message "Connected. Managing `%s' buffers in project %s." - managed-major-mode short-name) + (lambda (proc) + (eglot--message "Connected! Process `%s' now managing `%s'\ +buffers in project %s." + proc + managed-major-mode + short-name) (dolist (buffer (buffer-list)) (with-current-buffer buffer - (when(and buffer-file-name - (cl-some - (lambda (root) - (string-prefix-p - (expand-file-name root) - (expand-file-name buffer-file-name))) - (project-roots project))) - (unless eglot-editing-mode - (eglot-editing-mode 1)) - (eglot--signalDidOpen))))))))))) + (eglot--maybe-activate-editing-mode proc)))))))))) (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." @@ -706,7 +700,7 @@ running. INTERACTIVE is t if called interactively." -;;; Mode line +;;; Minor modes and mode-line ;;; (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -717,6 +711,20 @@ running. INTERACTIVE is t if called interactively." (defvar eglot-editing-mode-map (make-sparse-keymap)) +(defun eglot--maybe-activate-editing-mode (&optional proc) + "Maybe activate mode function `eglot-editing-mode'. +If PROC is supplied, do it only if BUFFER is managed by it. In +that case, also signal textDocument/didOpen." + (when buffer-file-name + (let ((cur (eglot--current-process))) + (when (or (and (null proc) cur) + (and proc (eq proc cur))) + (unless eglot-editing-mode + (eglot-editing-mode 1)) + (eglot--signalDidOpen))))) + +(add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) + (define-minor-mode eglot-editing-mode "Minor mode for source buffers where EGLOT helps you edit." nil From 65f421f724dbe50218f3cfbda6d6d5451174cdc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 14:49:43 +0100 Subject: [PATCH 043/771] Fix assorted bugs * eglot.el (eglot--special-buffer-process): Must be buffer-local. (eglot--define-process-var): Fix disaster waiting to happen. (eglot--process-receive): Explicitly pass PROC to eglot--pending-continuations. (eglot--textDocument/publishDiagnostics): Clear unreported diagnostics --- lisp/progmodes/eglot.el | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f4ac8584064..5c014f325b9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -48,7 +48,7 @@ (defvar eglot-editing-mode) ; forward decl (defvar eglot-mode) ; forward decl -(defvar eglot--special-buffer-process nil +(defvar-local eglot--special-buffer-process nil "Current buffer's eglot process.") (defun eglot--current-process () @@ -78,13 +78,14 @@ after setting it." (declare (indent 2)) `(progn (put ',var-sym 'function-documentation ,doc) - (defun ,var-sym (&optional process) - (let* ((proc (or process (eglot--current-process-or-lose))) - (probe (process-get proc ',var-sym))) - (or probe - (let ((def ,initval)) - (process-put proc ',var-sym def) - def)))) + (defun ,var-sym (proc) + (let* ((plist (process-plist proc)) + (probe (plist-member plist ',var-sym))) + (if probe + (cadr probe) + (let ((def ,initval)) + (process-put proc ',var-sym def) + def)))) (gv-define-setter ,var-sym (to-store &optional process) (let* ((prop ',var-sym)) ,(let ((form '(let ((proc (or ,process (eglot--current-process-or-lose)))) @@ -417,7 +418,8 @@ identifier. ERROR is non-nil if this is an error." (let* ((response-id (plist-get message :id)) (err (plist-get message :error)) (continuations (and response-id - (gethash response-id (eglot--pending-continuations))))) + (gethash response-id + (eglot--pending-continuations proc))))) (eglot--log-event proc (cond ((not response-id) 'server-notification) @@ -436,7 +438,7 @@ identifier. ERROR is non-nil if this is an error." (continuations (cancel-timer (cl-third continuations)) (remhash response-id - (eglot--pending-continuations)) + (eglot--pending-continuations proc)) (cond (err (apply (cl-second continuations) err)) (t @@ -655,11 +657,12 @@ running. INTERACTIVE is t if called interactively." message)))) into diags finally - (if eglot--current-flymake-report-fn - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics - diags)))))) + (if (null eglot--current-flymake-report-fn) + (setq eglot--unreported-diagnostics + diags) + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics nil)))))) (t (eglot--message "OK so %s isn't visited" filename))))) From dd7ce8988a1fe90b9fcb6a241382b9dcfebacabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 14:54:39 +0100 Subject: [PATCH 044/771] Make m-x eglot the main entry point * eglot.el (eglot-new-process): Removed (eglot): Rename from eglot-new-process. (eglot-editing-mode): Mention M-x eglot * README.md: Use M-x eglot --- lisp/progmodes/eglot.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5c014f325b9..ccf4b723d3d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -208,9 +208,9 @@ INTERACTIVE is t if called interactively." (eglot--message "Reconnected!")))) (defvar eglot--command-history nil - "History of COMMAND arguments to `eglot-new-process'.") + "History of COMMAND arguments to `eglot'.") -(defun eglot-new-process (managed-major-mode command &optional interactive) +(defun eglot (managed-major-mode command &optional interactive) ;; FIXME: Later make this function also connect to TCP servers by ;; overloading semantics on COMMAND. "Start a Language Server Protocol server. @@ -252,7 +252,7 @@ INTERACTIVE is t if called interactively." (let* ((project (project-current)) (short-name (eglot--project-short-name project))) (unless project (eglot--error - "(new-process) Cannot work without a current project!")) + "Cannot work without a current project!")) (let ((current-process (eglot--current-process)) (command (or command @@ -742,7 +742,7 @@ that case, also signal textDocument/didOpen." (flymake-mode 1) (if (eglot--current-process) (eglot--signalDidOpen) - (eglot--warn "No process, start one with `M-x eglot-new-process'"))) + (eglot--warn "No process, start one with `M-x eglot'"))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) From 878922319e7223888033a1dec9b3cc032573346c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 15:10:32 +0100 Subject: [PATCH 045/771] Fix another flymake sync bug * eglot.el (eglot-flymake-backend): Only report unreported sometimes. (eglot--maybe-activate-editing-mode): Start flymake explicitly when didOpen. (eglot--textDocument/publishDiagnostics): No need to set unreported-diagnostics to nil. (flymake): Require it. --- lisp/progmodes/eglot.el | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ccf4b723d3d..3561da2db46 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -32,6 +32,7 @@ (require 'pcase) (require 'compile) ; for some faces (require 'warnings) +(require 'flymake) (defgroup eglot nil "Interaction with Language Server Protocol servers" @@ -657,12 +658,11 @@ running. INTERACTIVE is t if called interactively." message)))) into diags finally - (if (null eglot--current-flymake-report-fn) - (setq eglot--unreported-diagnostics - diags) - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics nil)))))) + (if eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics + diags)))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -724,7 +724,8 @@ that case, also signal textDocument/didOpen." (and proc (eq proc cur))) (unless eglot-editing-mode (eglot-editing-mode 1)) - (eglot--signalDidOpen))))) + (eglot--signalDidOpen) + (flymake-start))))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -947,10 +948,11 @@ Records START, END and LENGTH locally." (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." - ;; Call immediately with anything unreported (this will clear any - ;; pending diags) - (funcall report-fn eglot--unreported-diagnostics) - (setq eglot--unreported-diagnostics nil) + ;; Maybe call immediately if anything unreported (this will clear + ;; any pending diags) + (when eglot--unreported-diagnostics + (funcall report-fn eglot--unreported-diagnostics) + (setq eglot--unreported-diagnostics nil)) ;; Setup so maybe it's called later, too. (setq eglot--current-flymake-report-fn report-fn) ;; Take this opportunity to signal a didChange that might eventually From e366550f05aefd1eddbb363f43fdf6b567e9650f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 15:45:30 +0100 Subject: [PATCH 046/771] Must re-announce didopen after reconnect * eglot.el (eglot-reconnect): Also call eglot--maybe-activate-editing-mode for all buffers. --- lisp/progmodes/eglot.el | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3561da2db46..86c0a4c05fa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -205,8 +205,11 @@ INTERACTIVE is t if called interactively." (eglot--major-mode process) (eglot--short-name process) (eglot--bootstrap-fn process) - (lambda (_proc) - (eglot--message "Reconnected!")))) + (lambda (proc) + (eglot--message "Reconnected!") + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc)))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") From f706000c89019e646cf55a417b3b87035ed32794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 15:46:27 +0100 Subject: [PATCH 047/771] Fix flymake diagnostic positions It's better not to use flymake-diag-region here. * eglot.el (eglot--textDocument/publishDiagnostics): Calculate position by hand. --- lisp/progmodes/eglot.el | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 86c0a4c05fa..bb0427aca73 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -637,10 +637,13 @@ running. INTERACTIVE is t if called interactively." (with-current-buffer buffer (cl-flet ((pos-at (pos-plist) - (car (flymake-diag-region - (current-buffer) - (plist-get pos-plist :line) - (plist-get pos-plist :character))))) + (save-excursion + (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char + (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position))))))) (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range severity _code _source message) From 58c19b7683263548b564696df61ea67b6fd96aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 15:47:39 +0100 Subject: [PATCH 048/771] Delete two useless forward declarations * eglot.el (eglot-mode, eglot-editing-mode-map): Remove forward decls. --- lisp/progmodes/eglot.el | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bb0427aca73..f25c7bd3466 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -46,9 +46,6 @@ (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defvar eglot-editing-mode) ; forward decl -(defvar eglot-mode) ; forward decl - (defvar-local eglot--special-buffer-process nil "Current buffer's eglot process.") @@ -643,7 +640,8 @@ running. INTERACTIVE is t if called interactively." (forward-char (min (plist-get pos-plist :character) (- (line-end-position) - (line-beginning-position))))))) + (line-beginning-position)))) + (point)))) (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range severity _code _source message) From 4d1c9b903d70283e88a3f35f2ebf6704aa127cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 16:04:03 +0100 Subject: [PATCH 049/771] Reorganize file * eglot.el (eglot-mode-line): Move up. (eglot-make-local-process, eglot--all-major-modes, eglot--obj) (eglot--project-short-name, eglot--all-major-modes) (eglot-reconnect, eglot--maybe-activate-editing-mode) (eglot--protocol-initialize) (eglot--window/showMessage, eglot--current-flymake-report-fn) (eglot--unreported-diagnostics) (eglot--textDocument/publishDiagnostics, eglot--signalDidOpen) (eglot--signalDidClose): Move around. (eglot-quit-server): Renamed to eglot-shutdown. (eglot-shutdown): New function --- lisp/progmodes/eglot.el | 391 ++++++++++++++++++++-------------------- 1 file changed, 198 insertions(+), 193 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f25c7bd3466..7d44e6dd277 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -34,6 +34,8 @@ (require 'warnings) (require 'flymake) + +;;; User tweakable stuff (defgroup eglot nil "Interaction with Language Server Protocol servers" :prefix "eglot-" @@ -43,6 +45,13 @@ (python-mode . ("pyls"))) "Alist mapping major modes to server executables.") +(defface eglot-mode-line + '((t (:inherit font-lock-constant-face :weight bold))) + "Face for package-name in EGLOT's mode line." + :group 'eglot) + + +;;; Process management (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") @@ -129,20 +138,6 @@ A list (WHAT SERIOUS-P)." t) Must be a function of one arg, a name, returning a process object.") -(defun eglot--project-short-name (project) - "Give PROJECT a short name." - (file-name-base - (directory-file-name - (car (project-roots project))))) - -(defun eglot--all-major-modes () - "Return all know major modes." - (let ((retval)) - (mapatoms (lambda (sym) - (when (plist-member (symbol-plist sym) 'derived-mode-parent) - (push sym retval)))) - retval)) - (defun eglot-make-local-process (name command) "Make a local LSP process from COMMAND. NAME is a name to give the inferior process or connection. @@ -161,6 +156,26 @@ Returns a process object." name))))) proc)) +(defmacro eglot--obj (&rest what) + "Make WHAT a suitable argument for `json-encode'." + ;; FIXME: maybe later actually do something, for now this just fixes + ;; the indenting of literal plists. + `(list ,@what)) + +(defun eglot--project-short-name (project) + "Give PROJECT a short name." + (file-name-base + (directory-file-name + (car (project-roots project))))) + +(defun eglot--all-major-modes () + "Return all know major modes." + (let ((retval)) + (mapatoms (lambda (sym) + (when (plist-member (symbol-plist sym) 'derived-mode-parent) + (push sym retval)))) + retval)) + (defun eglot--connect (project managed-major-mode short-name bootstrap-fn &optional success-fn) "Make a connection for PROJECT, SHORT-NAME and MANAGED-MAJOR-MODE. @@ -191,23 +206,6 @@ SUCCESS-FN with no args if all goes well." (setf (eglot--status proc) nil) (when success-fn (funcall success-fn proc))))))))) -(defun eglot-reconnect (process &optional interactive) - "Reconnect to PROCESS. -INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (when (process-live-p process) - (eglot-quit-server process 'sync interactive)) - (eglot--connect - (eglot--project process) - (eglot--major-mode process) - (eglot--short-name process) - (eglot--bootstrap-fn process) - (lambda (proc) - (eglot--message "Reconnected!") - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc)))))) - (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -285,6 +283,23 @@ buffers in project %s." (with-current-buffer buffer (eglot--maybe-activate-editing-mode proc)))))))))) +(defun eglot-reconnect (process &optional interactive) + "Reconnect to PROCESS. +INTERACTIVE is t if called interactively." + (interactive (list (eglot--current-process-or-lose) t)) + (when (process-live-p process) + (eglot-shutdown process 'sync interactive)) + (eglot--connect + (eglot--project process) + (eglot--major-mode process) + (eglot--short-name process) + (eglot--bootstrap-fn process) + (lambda (proc) + (eglot--message "Reconnected!") + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc)))))) + (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) @@ -370,12 +385,6 @@ buffers in project %s." ;; (setf (eglot--expected-bytes proc) expected-bytes))))) -(defmacro eglot--obj (&rest what) - "Make WHAT a suitable argument for `json-encode'." - ;; FIXME: maybe later actually do something, for now this just fixes - ;; the indenting of literal plists. - `(list ,@what)) - (defun eglot-events-buffer (process &optional interactive) "Display events buffer for current LSP connection PROCESS. INTERACTIVE is t if called interactively." @@ -561,125 +570,6 @@ identifier. ERROR is non-nil if this is an error." :method method :params params))) - -;;; Requests -;;; -(defun eglot--protocol-initialize (process success-fn) - "Initialize LSP protocol. -PROCESS is a connected process (network or local). SUCCESS-FN is -called with capabilites after connection." - (eglot--request - process - :initialize - (eglot--obj :processId (emacs-pid) - :rootPath (concat - (expand-file-name (car (project-roots - (project-current))))) - :initializationOptions [] - :capabilities - (eglot--obj - :workspace (eglot--obj) - :textDocument (eglot--obj - :publishDiagnostics `(:relatedInformation t)))) - :success-fn success-fn)) - -(defun eglot-quit-server (process &optional sync interactive) - "Politely ask the server PROCESS to quit. -If SYNC, don't leave this function with the server still -running. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t t)) - (when interactive - (eglot--message "(eglot-quit-server) Asking %s politely to terminate" - process)) - (let ((brutal (lambda () - (eglot--warn "Brutally deleting existing process %s" - process) - (setf (eglot--moribund process) t) - (delete-process process)))) - (eglot--request - process - :shutdown - nil - :success-fn (lambda (&rest _anything) - (when interactive - (eglot--message "Now asking %s politely to exit" process)) - (setf (eglot--moribund process) t) - (eglot--request process - :exit - nil - :success-fn brutal - :async-p (not sync) - :error-fn brutal - :timeout-fn brutal)) - :error-fn brutal - :async-p (not sync) - :timeout-fn brutal))) - - -;;; Notifications -;;; -(defvar-local eglot--current-flymake-report-fn nil - "Current flymake report function for this buffer") -(defvar-local eglot--unreported-diagnostics nil - "Unreported diagnostics for this buffer.") - -(cl-defun eglot--textDocument/publishDiagnostics - (_process &key uri diagnostics) - "Handle notification publishDiagnostics" - (let* ((obj (url-generic-parse-url uri)) - (filename (car (url-path-and-query obj))) - (buffer (find-buffer-visiting filename))) - (cond - (buffer - (with-current-buffer buffer - (cl-flet ((pos-at - (pos-plist) - (save-excursion - (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) - (forward-char - (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) - (point)))) - (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity - _code _source message) - diag-spec - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (pos-at start)) - (end-pos (pos-at end))) - (flymake-make-diagnostic - (current-buffer) - begin-pos end-pos - (cond ((<= severity 1) - :error) - ((= severity 2) - :warning) - (t - :note)) - message)))) - into diags - finally - (if eglot--current-flymake-report-fn - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics - diags)))))) - (t - (eglot--message "OK so %s isn't visited" filename))))) - -(cl-defun eglot--window/showMessage - (process &key type message) - "Handle notification window/showMessage" - (when (<= 1 type) - (setf (eglot--status process) '("error" t)) - (eglot--log-event process - (propertize "server-error" 'face 'error) - message)) - (eglot--message "Server reports (type=%s): %s" type message)) - ;;; Helpers ;;; @@ -705,34 +595,13 @@ running. INTERACTIVE is t if called interactively." (apply #'format format args) :warning))) - -;;; Minor modes and mode-line +;;; Minor modes ;;; -(defface eglot-mode-line - '((t (:inherit font-lock-constant-face :weight bold))) - "Face for package-name in EGLOT's mode line." - :group 'eglot) - (defvar eglot-mode-map (make-sparse-keymap)) (defvar eglot-editing-mode-map (make-sparse-keymap)) -(defun eglot--maybe-activate-editing-mode (&optional proc) - "Maybe activate mode function `eglot-editing-mode'. -If PROC is supplied, do it only if BUFFER is managed by it. In -that case, also signal textDocument/didOpen." - (when buffer-file-name - (let ((cur (eglot--current-process))) - (when (or (and (null proc) cur) - (and proc (eq proc cur))) - (unless eglot-editing-mode - (eglot-editing-mode 1)) - (eglot--signalDidOpen) - (flymake-start))))) - -(add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) - (define-minor-mode eglot-editing-mode "Minor mode for source buffers where EGLOT helps you edit." nil @@ -764,6 +633,24 @@ that case, also signal textDocument/didOpen." (when eglot-editing-mode (eglot-editing-mode -1))))) +(defun eglot--maybe-activate-editing-mode (&optional proc) + "Maybe activate mode function `eglot-editing-mode'. +If PROC is supplied, do it only if BUFFER is managed by it. In +that case, also signal textDocument/didOpen." + (when buffer-file-name + (let ((cur (eglot--current-process))) + (when (or (and (null proc) cur) + (and proc (eq proc cur))) + (unless eglot-editing-mode + (eglot-editing-mode 1)) + (eglot--signalDidOpen) + (flymake-start))))) + +(add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) + + +;;; Mode-line, menu and other sugar +;;; (defvar eglot-menu) (easy-menu-define eglot-menu eglot-mode-map "EGLOT" @@ -806,7 +693,7 @@ that case, also signal textDocument/didOpen." face eglot-mode-line keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] 'eglot-events-buffer) - (define-key map [mode-line mouse-2] 'eglot-quit-server) + (define-key map [mode-line mouse-2] 'eglot-shutdown) (define-key map [mode-line mouse-3] 'eglot-reconnect) map) mouse-face mode-line-highlight @@ -863,6 +750,124 @@ that case, also signal textDocument/didOpen." `(eglot-mode (" [" eglot--mode-line-format "] "))) + +;;; Protocol implementation (Requests, notifications, etc) +;;; +(defun eglot--protocol-initialize (process success-fn) + "Initialize LSP protocol. +PROCESS is a connected process (network or local). SUCCESS-FN is +called with capabilites after connection." + (eglot--request + process + :initialize + (eglot--obj :processId (emacs-pid) + :rootPath (concat + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities + (eglot--obj + :workspace (eglot--obj) + :textDocument (eglot--obj + :publishDiagnostics `(:relatedInformation t)))) + :success-fn success-fn)) + +(defun eglot-shutdown (process &optional sync interactive) + "Politely ask the server PROCESS to quit. +Forcefully quit it if it doesn't respond. +If SYNC, don't leave this function with the server still +running. INTERACTIVE is t if called interactively." + (interactive (list (eglot--current-process-or-lose) t t)) + (when interactive + (eglot--message "(eglot-shutdown) Asking %s politely to terminate" + process)) + (let ((brutal (lambda () + (eglot--warn "Brutally deleting existing process %s" + process) + (setf (eglot--moribund process) t) + (delete-process process)))) + (eglot--request + process + :shutdown + nil + :success-fn (lambda (&rest _anything) + (when interactive + (eglot--message "Now asking %s politely to exit" process)) + (setf (eglot--moribund process) t) + (eglot--request process + :exit + nil + :success-fn brutal + :async-p (not sync) + :error-fn brutal + :timeout-fn brutal)) + :error-fn brutal + :async-p (not sync) + :timeout-fn brutal))) + +(cl-defun eglot--window/showMessage + (process &key type message) + "Handle notification window/showMessage" + (when (<= 1 type) + (setf (eglot--status process) '("error" t)) + (eglot--log-event process + (propertize "server-error" 'face 'error) + message)) + (eglot--message "Server reports (type=%s): %s" type message)) + +(defvar-local eglot--current-flymake-report-fn nil + "Current flymake report function for this buffer") + +(defvar-local eglot--unreported-diagnostics nil + "Unreported diagnostics for this buffer.") + +(cl-defun eglot--textDocument/publishDiagnostics + (_process &key uri diagnostics) + "Handle notification publishDiagnostics" + (let* ((obj (url-generic-parse-url uri)) + (filename (car (url-path-and-query obj))) + (buffer (find-buffer-visiting filename))) + (cond + (buffer + (with-current-buffer buffer + (cl-flet ((pos-at + (pos-plist) + (save-excursion + (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char + (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position)))) + (point)))) + (cl-loop for diag-spec across diagnostics + collect (cl-destructuring-bind (&key range severity + _code _source message) + diag-spec + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (pos-at start)) + (end-pos (pos-at end))) + (flymake-make-diagnostic + (current-buffer) + begin-pos end-pos + (cond ((<= severity 1) + :error) + ((= severity 2) + :warning) + (t + :note)) + message)))) + into diags + finally + (if eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics + diags)))))) + (t + (eglot--message "OK so %s isn't visited" filename))))) + (defvar eglot--recent-changes nil "List of recent changes as collected by `eglot--after-change'.") @@ -902,20 +907,6 @@ Records START, END and LENGTH locally." ;; (eglot--message "start is %s, end is %s, length is %s" start end length) ) -(defun eglot--signalDidOpen () - "Send textDocument/didOpen to server." - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) - -(defun eglot--signalDidClose () - "Send textDocument/didClose to server." - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didClose - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) - (defun eglot--maybe-signal-didChange () "Send textDocument/didChange to server." (when eglot--recent-changes @@ -949,6 +940,20 @@ Records START, END and LENGTH locally." :text (buffer-substring-no-properties start end)))))))) (setq eglot--recent-changes nil))) +(defun eglot--signalDidOpen () + "Send textDocument/didOpen to server." + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didOpen + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) + +(defun eglot--signalDidClose () + "Send textDocument/didClose to server." + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didClose + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) + (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." From e5e9437882406c406ed401fc67faf97d523a0e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 16:14:35 +0100 Subject: [PATCH 050/771] Rename rpc methods for clarity * eglot.el (eglot--process-receive): Search for RPC server methods under `eglot--server-' (eglot-editing-mode, eglot--maybe-activate-editing-mode): Use new signal names. (eglot--server-window/showMessage): Rename from eglot--window/showMessage. (eglot--server-textDocument/publishDiagnostics): Renamed from eglot--textDocument/publishDiagnostics. (eglot--current-buffer-versioned-identifier): Remove. (eglot--current-buffer-VersionedTextDocumentIdentifier): Use eglot--versioned-identifier. (eglot--signal-textDocument/didChange): Renamed from eglot--maybe-signal-didChange. (eglot--signal-textDocument/didOpen): Renamed from eglot--signalDidOpen. (eglot--signal-textDocument/didClose): Rename from eglot--signalDidClose. (eglot-flymake-backend): Call eglot--signal-textDocument/didChange. (eglot--server-window/progress): Rename from eglot--window/progress. --- lisp/progmodes/eglot.el | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7d44e6dd277..469d3f704ac 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -455,12 +455,12 @@ identifier. ERROR is non-nil if this is an error." (apply (cl-first continuations) (plist-get message :result))))) (t (let* ((method (plist-get message :method)) - (handler-sym (intern (concat "eglot--" + (handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) (apply handler-sym proc (plist-get message :params)) - (eglot--debug "No implemetation for notification %s yet" - method))))))) + (eglot--warn "No implemetation for notification %s yet" + method))))))) (defvar eglot--expect-carriage-return nil) @@ -612,15 +612,15 @@ identifier. ERROR is non-nil if this is an error." (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) - (add-hook 'kill-buffer-hook 'eglot--signalDidClose nil t) + (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (flymake-mode 1) (if (eglot--current-process) - (eglot--signalDidOpen) + (eglot--signal-textDocument/didOpen) (eglot--warn "No process, start one with `M-x eglot'"))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) - (remove-hook 'kill-buffer-hook 'eglot--signalDidClose t)))) + (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -643,7 +643,7 @@ that case, also signal textDocument/didOpen." (and proc (eq proc cur))) (unless eglot-editing-mode (eglot-editing-mode 1)) - (eglot--signalDidOpen) + (eglot--signal-textDocument/didOpen) (flymake-start))))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -805,7 +805,7 @@ running. INTERACTIVE is t if called interactively." :async-p (not sync) :timeout-fn brutal))) -(cl-defun eglot--window/showMessage +(cl-defun eglot--server-window/showMessage (process &key type message) "Handle notification window/showMessage" (when (<= 1 type) @@ -821,7 +821,7 @@ running. INTERACTIVE is t if called interactively." (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") -(cl-defun eglot--textDocument/publishDiagnostics +(cl-defun eglot--server-textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" (let* ((obj (url-generic-parse-url uri)) @@ -873,11 +873,6 @@ running. INTERACTIVE is t if called interactively." (defvar-local eglot--versioned-identifier 0) -(defun eglot--current-buffer-versioned-identifier () - "Return a VersionedTextDocumentIdentifier." - ;; FIXME: later deal with workspaces - eglot--versioned-identifier) - (defun eglot--current-buffer-VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (eglot--obj :uri @@ -885,7 +880,8 @@ running. INTERACTIVE is t if called interactively." (url-hexify-string (file-truename buffer-file-name) url-path-allowed-chars)) - :version (eglot--current-buffer-versioned-identifier))) + ;; FIXME: later deal with workspaces + :version eglot--versioned-identifier)) (defun eglot--current-buffer-TextDocumentItem () "Compute TextDocumentItem object for current buffer." @@ -907,7 +903,7 @@ Records START, END and LENGTH locally." ;; (eglot--message "start is %s, end is %s, length is %s" start end length) ) -(defun eglot--maybe-signal-didChange () +(defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when eglot--recent-changes (save-excursion @@ -940,14 +936,14 @@ Records START, END and LENGTH locally." :text (buffer-substring-no-properties start end)))))))) (setq eglot--recent-changes nil))) -(defun eglot--signalDidOpen () +(defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) -(defun eglot--signalDidClose () +(defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didClose @@ -966,12 +962,12 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (setq eglot--current-flymake-report-fn report-fn) ;; Take this opportunity to signal a didChange that might eventually ;; make the server report new diagnostics. - (eglot--maybe-signal-didChange)) + (eglot--signal-textDocument/didChange)) ;;; Rust-specific ;;; -(cl-defun eglot--window/progress +(cl-defun eglot--server-window/progress (process &key id done title ) "Handle notification window/progress" (setf (eglot--spinner process) (list id title done))) From 22965312e117a8f864a23e223f32c29cae0fdba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 22:42:13 +0100 Subject: [PATCH 051/771] Fix textdocument/didchange * eglot.el (eglot-editing-mode): Manage before-change-functions. (eglot--recent-changes): Deleted. (eglot--recent-before-changes): New var. (eglot--recent-after-changes): Renamed from eglot--recent-changes. (eglot--pos-to-lsp-position, eglot--before-change): New helpers. (eglot--after-change): Rework. (eglot--signal-textDocument/didChange): Rework. --- lisp/progmodes/eglot.el | 98 +++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 469d3f704ac..1bf0d561c35 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -611,6 +611,7 @@ identifier. ERROR is non-nil if this is an error." (eglot-editing-mode (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) + (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (flymake-mode 1) @@ -620,6 +621,7 @@ identifier. ERROR is non-nil if this is an error." (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) + (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t)))) (define-minor-mode eglot-mode @@ -868,7 +870,9 @@ running. INTERACTIVE is t if called interactively." (t (eglot--message "OK so %s isn't visited" filename))))) -(defvar eglot--recent-changes nil +(defvar eglot--recent-before-changes nil + "List of recent changes as collected by `eglot--before-change'.") +(defvar eglot--recent-after-changes nil "List of recent changes as collected by `eglot--after-change'.") (defvar-local eglot--versioned-identifier 0) @@ -895,46 +899,76 @@ running. INTERACTIVE is t if called interactively." (widen) (buffer-substring-no-properties (point-min) (point-max)))))) -(defun eglot--after-change (start end length) +(defun eglot--pos-to-lsp-position (pos) + "Convert point POS to LSP position." + (save-excursion + (eglot--obj :line + ;; F!@(#*&#$)CKING OFF-BY-ONE + (1- (line-number-at-pos pos t)) + :character + (- (goto-char pos) + (line-beginning-position))))) + +(defun eglot--before-change (start end) + "Hook onto `before-change-functions'. +Records START and END, crucially convert them into +LSP (line/char) positions before that information is +lost (because the after-change thingy doesn't know if newlines +were deleted/added)" + (push (list (eglot--pos-to-lsp-position start) + (eglot--pos-to-lsp-position end)) + eglot--recent-before-changes)) + +(defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. -Records START, END and LENGTH locally." +Records START, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (push (list start end length) eglot--recent-changes) - ;; (eglot--message "start is %s, end is %s, length is %s" start end length) - ) + (push (list start end pre-change-length) eglot--recent-after-changes)) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." - (when eglot--recent-changes + (when (and eglot--recent-before-changes + eglot--recent-after-changes) (save-excursion (save-restriction (widen) - (let* ((start (cl-reduce #'min (mapcar #'car eglot--recent-changes))) - (end (cl-reduce #'max (mapcar #'cadr eglot--recent-changes)))) - (eglot--notify - (eglot--current-process-or-lose) - :textDocument/didChange - (eglot--obj - :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges - (vector + (if (/= (length eglot--recent-before-changes) + (length eglot--recent-after-changes)) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didChange (eglot--obj - :range (eglot--obj - :start - (eglot--obj :line - (line-number-at-pos start t) - :character - (- (goto-char start) - (line-beginning-position))) - :end - (eglot--obj :line - (line-number-at-pos end t) - :character - (- (goto-char end) - (line-beginning-position)))) - :rangeLength (- end start) - :text (buffer-substring-no-properties start end)))))))) - (setq eglot--recent-changes nil))) + :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (vector + (eglot--obj + :text (buffer-substring-no-properties (point-min) (point-max)))))) + (let ((combined (cl-mapcar 'append + eglot--recent-before-changes + eglot--recent-after-changes))) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didChange + (eglot--obj + :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (apply + #'vector + (mapcar (pcase-lambda (`(,before-start-position + ,before-end-position + ,after-start + ,after-end + ,len)) + (eglot--obj + :range + (eglot--obj + :start before-start-position + :end before-end-position) + :rangeLength len + :text (buffer-substring-no-properties after-start after-end))) + (reverse combined)))))))))) + (setq eglot--recent-before-changes nil + eglot--recent-after-changes nil)) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." From 6c84a2e8cbb1a1e9881710edc26c86f7947f2dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 22:44:13 +0100 Subject: [PATCH 052/771] Fix a couple of rust-related edge cases * eglot.el (eglot--server-window/progress): Allow other keys. (eglot--server-textDocument/publishDiagnostics): Allow :group in diagnostic spec. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1bf0d561c35..e7fe18ccf4c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -843,7 +843,7 @@ running. INTERACTIVE is t if called interactively." (line-beginning-position)))) (point)))) (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity + collect (cl-destructuring-bind (&key range severity _group _code _source message) diag-spec (cl-destructuring-bind (&key start end) @@ -1002,7 +1002,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;;; Rust-specific ;;; (cl-defun eglot--server-window/progress - (process &key id done title ) + (process &key id done title &allow-other-keys) "Handle notification window/progress" (setf (eglot--spinner process) (list id title done))) From 009062feb70d5d9ea0c3c68f7c1a977bdb4c80bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 3 May 2018 23:59:56 +0100 Subject: [PATCH 053/771] Trim some edges and add a bunch of boring rpc methods * eglot.el (eglot--connect): Don't call eglot--protocol-initialize. (eglot--process-filter): Break long line. (eglot--process-receive): Also pass id to handler if a server request. (eglot--log): New helper. (eglot-editing-mode): Manage before-revert-hook, after-revert-hook, before-save-hook, after-save-hook. (eglot--protocol-initialize): Removed. (eglot--server-window/showMessage): Simplify. (eglot--server-window/showMessageRequest) (eglot--server-window/logMessage, eglot--server-telemetry/event): New handlers. (eglot--signal-textDocument/willSave) (eglot--signal-textDocument/didSave): New notifications. (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didClose): Check eglot--buffer-open-count. (eglot--buffer-open-count): New var. --- lisp/progmodes/eglot.el | 135 ++++++++++++++++++++++++++++++---------- 1 file changed, 102 insertions(+), 33 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e7fe18ccf4c..ebc07d21622 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -198,13 +198,26 @@ SUCCESS-FN with no args if all goes well." (let ((inhibit-read-only t)) (insert (format "\n-----------------------------------\n")))) - (eglot--protocol-initialize + (eglot--request proc + :initialize + (eglot--obj :processId (emacs-pid) + :rootPath (concat + (expand-file-name (car (project-roots + (project-current))))) + :initializationOptions [] + :capabilities + (eglot--obj + :workspace (eglot--obj) + :textDocument (eglot--obj + :publishDiagnostics `(:relatedInformation t)))) + :success-fn (cl-function (lambda (&key capabilities) (setf (eglot--capabilities proc) capabilities) (setf (eglot--status proc) nil) - (when success-fn (funcall success-fn proc))))))))) + (when success-fn (funcall success-fn proc)) + (eglot--notify proc :initialized nil)))))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -347,7 +360,8 @@ INTERACTIVE is t if called interactively." ;; (setq expected-bytes (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" + "\\(?:.*: .*\r\n\\)*Content-Length: \ +*\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" (+ (point) 100) t) (string-to-number (match-string 1)))) @@ -454,11 +468,15 @@ identifier. ERROR is non-nil if this is an error." (t (apply (cl-first continuations) (plist-get message :result))))) (t + ;; a server notification or a server request (let* ((method (plist-get message :method)) (handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) - (apply handler-sym proc (plist-get message :params)) + (apply handler-sym proc (append + (plist-get message :params) + (let ((id (plist-get message :id))) + (if id `(:id ,id))))) (eglot--warn "No implemetation for notification %s yet" method))))))) @@ -587,6 +605,10 @@ identifier. ERROR is non-nil if this is an error." "Message out with FORMAT with ARGS." (message (concat "[eglot] " (apply #'format format args)))) +(defun eglot--log (format &rest args) + "Log out with FORMAT with ARGS." + (message (concat "[eglot-log] " (apply #'format format args)))) + (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." (apply #'eglot--message (concat "(warning) " format) args) @@ -614,15 +636,22 @@ identifier. ERROR is non-nil if this is an error." (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) + (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) + (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) + (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) + (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (flymake-mode 1) - (if (eglot--current-process) - (eglot--signal-textDocument/didOpen) + (unless (eglot--current-process) (eglot--warn "No process, start one with `M-x eglot'"))) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) - (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t)))) + (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) + (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) + (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) + (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) + (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -755,25 +784,6 @@ that case, also signal textDocument/didOpen." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot--protocol-initialize (process success-fn) - "Initialize LSP protocol. -PROCESS is a connected process (network or local). SUCCESS-FN is -called with capabilites after connection." - (eglot--request - process - :initialize - (eglot--obj :processId (emacs-pid) - :rootPath (concat - (expand-file-name (car (project-roots - (project-current))))) - :initializationOptions [] - :capabilities - (eglot--obj - :workspace (eglot--obj) - :textDocument (eglot--obj - :publishDiagnostics `(:relatedInformation t)))) - :success-fn success-fn)) - (defun eglot-shutdown (process &optional sync interactive) "Politely ask the server PROCESS to quit. Forcefully quit it if it doesn't respond. @@ -808,14 +818,47 @@ running. INTERACTIVE is t if called interactively." :timeout-fn brutal))) (cl-defun eglot--server-window/showMessage - (process &key type message) + (_process &key type message) "Handle notification window/showMessage" - (when (<= 1 type) - (setf (eglot--status process) '("error" t)) - (eglot--log-event process - (propertize "server-error" 'face 'error) - message)) - (eglot--message "Server reports (type=%s): %s" type message)) + (eglot--message (propertize "Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message)) + +(cl-defun eglot--server-window/showMessageRequest + (process &key id type message actions) + "Handle server request window/showMessageRequest" + (let (reply) + (unwind-protect + (setq reply + (completing-read + (concat + (format (propertize "[eglot] Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message) + "\nChoose an option: ") + (mapcar (lambda (obj) (plist-get obj :title)) actions) + nil + t + (plist-get (elt actions 0) :title))) + (eglot--process-send + id + process + (if reply + (eglot--obj :result (eglot--obj :title reply)) + ;; request cancelled + (eglot--obj :error -32800)))))) + +(cl-defun eglot--server-window/logMessage + (_process &key type message) + "Handle notification window/logMessage" + (eglot--log (propertize "Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message)) + +(cl-defun eglot--server-telemetry/event + (_process &rest any) + "Handle notification telemetry/event" + (eglot--log "Server telemetry: %s" any)) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -970,8 +1013,12 @@ Records START, END and PRE-CHANGE-LENGTH locally." (setq eglot--recent-before-changes nil eglot--recent-after-changes nil)) +(defvar-local eglot--buffer-open-count 0) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." + (cl-incf eglot--buffer-open-count) + (when (> eglot--buffer-open-count 1) + (error "Too many textDocument/didOpen notifs for %s" (current-buffer))) (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument @@ -979,11 +1026,33 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." + (cl-decf eglot--buffer-open-count) + (when (< eglot--buffer-open-count 0) + (error "Too many textDocument/didClose notifs for %s" (current-buffer))) (eglot--notify (eglot--current-process-or-lose) :textDocument/didClose (eglot--obj :textDocument (eglot--current-buffer-TextDocumentItem)))) +(defun eglot--signal-textDocument/willSave () + "Send textDocument/willSave to server." + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/willSave + (eglot--obj + :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha + :textDocument (eglot--current-buffer-TextDocumentItem)))) + +(defun eglot--signal-textDocument/didSave () + "Send textDocument/didSave to server." + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didSave + (eglot--obj + ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. + :text (buffer-substring-no-properties (point-min) (point-max)) + :textDocument (eglot--current-buffer-TextDocumentItem)))) + (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." From 0a587a881afc76b479fc18d0f0c52e0b32998d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 00:12:53 +0100 Subject: [PATCH 054/771] * eglot.el (eglot--process-receive): skip null method notifs. --- lisp/progmodes/eglot.el | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ebc07d21622..693535ac8b1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -477,8 +477,11 @@ identifier. ERROR is non-nil if this is an error." (plist-get message :params) (let ((id (plist-get message :id))) (if id `(:id ,id))))) - (eglot--warn "No implemetation for notification %s yet" - method))))))) + ;; pyls keeps on sending nil notifs for each notif we + ;; send it, just ignore these. + (unless (null method) + (eglot--warn "No implemetation for notification %s yet" + method)))))))) (defvar eglot--expect-carriage-return nil) From 465635456c581a7bc667fc1333ce463e19262a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 00:25:03 +0100 Subject: [PATCH 055/771] Fix mode-line mouse-clicks from outside selected window * eglot.el (eglot--mode-line-call): New helper. (eglot--mode-line-format): Use it. --- lisp/progmodes/eglot.el | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 693535ac8b1..bdd339d9715 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -695,6 +695,13 @@ that case, also signal textDocument/didOpen." (put 'eglot--mode-line-format 'risky-local-variable t) +(defun eglot--mode-line-call (what) + "Make an interactive lambda for calling WHAT from mode-line." + (lambda (event) + (interactive "e") + (with-selected-window (posn-window (event-start event)) + (call-interactively what)))) + (defun eglot--mode-line-format () "Compose the mode-line format spec." (pcase-let* ((proc (eglot--current-process)) @@ -715,7 +722,7 @@ that case, also signal textDocument/didOpen." face eglot-mode-line keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line down-mouse-1] - eglot-menu) + (eglot--mode-line-call 'eglot-menu)) map) mouse-face mode-line-highlight help-echo "mouse-1: pop-up EGLOT menu" @@ -726,9 +733,12 @@ that case, also signal textDocument/didOpen." ,name face eglot-mode-line keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] 'eglot-events-buffer) - (define-key map [mode-line mouse-2] 'eglot-shutdown) - (define-key map [mode-line mouse-3] 'eglot-reconnect) + (define-key map [mode-line mouse-1] + (eglot--mode-line-call 'eglot-events-buffer)) + (define-key map [mode-line mouse-2] + (eglot--mode-line-call 'eglot-shutdown)) + (define-key map [mode-line mouse-3] + (eglot--mode-line-call 'eglot-reconnect)) map) mouse-face mode-line-highlight help-echo ,(concat "mouse-1: go to events buffer\n" @@ -744,9 +754,9 @@ that case, also signal textDocument/didOpen." face compilation-mode-line-fail keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] - 'eglot-events-buffer) + (eglot--mode-line-call 'eglot-events-buffer)) (define-key map [mode-line mouse-3] - 'eglot-clear-status) + (eglot--mode-line-call 'eglot-clear-status)) map)))) ,@(when (and doing (not done-p)) `("/" @@ -757,7 +767,7 @@ that case, also signal textDocument/didOpen." face compilation-mode-line-run keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] - 'eglot-events-buffer) + (eglot--mode-line-call 'eglot-events-buffer)) map)))) ,@(when (cl-plusp pending) `("/" @@ -775,9 +785,9 @@ that case, also signal textDocument/didOpen." 'eglot-mode-line)) keymap ,(let ((map (make-sparse-keymap))) (define-key map [mode-line mouse-1] - 'eglot-events-buffer) + (eglot--mode-line-call 'eglot-events-buffer)) (define-key map [mode-line mouse-3] - 'eglot-forget-pending-continuations) + (eglot--mode-line-call 'eglot-forget-pending-continuations)) map))))))))) (add-to-list 'mode-line-misc-info From 0d002553edc2f2189d0bf0d84fc981215192d96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 01:20:13 +0100 Subject: [PATCH 056/771] More correctly keep track of didopen/didclose per buffer * eglot.el (eglot--buffer-open-count): Now a process-local var. (eglot--signal-textDocument/didOpen, eglot--signal-textDocument/didClose): Use it. --- lisp/progmodes/eglot.el | 42 ++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bdd339d9715..ac52b9adab9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -138,6 +138,9 @@ A list (WHAT SERIOUS-P)." t) Must be a function of one arg, a name, returning a process object.") +(eglot--define-process-var eglot--buffer-open-count (make-hash-table) + "Keeps track of didOpen/didClose notifs for each buffer.") + (defun eglot-make-local-process (name command) "Make a local LSP process from COMMAND. NAME is a name to give the inferior process or connection. @@ -1026,26 +1029,35 @@ Records START, END and PRE-CHANGE-LENGTH locally." (setq eglot--recent-before-changes nil eglot--recent-after-changes nil)) -(defvar-local eglot--buffer-open-count 0) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." - (cl-incf eglot--buffer-open-count) - (when (> eglot--buffer-open-count 1) - (error "Too many textDocument/didOpen notifs for %s" (current-buffer))) - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) + (let* ((proc (eglot--current-process-or-lose)) + (count (1+ (or (gethash (current-buffer) + (eglot--buffer-open-count proc)) + 0)))) + (when (> count 1) + (eglot--error "Too many textDocument/didOpen notifs for %s" (current-buffer))) + (setf (gethash (current-buffer) (eglot--buffer-open-count proc)) + count) + (eglot--notify proc + :textDocument/didOpen + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem))))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (cl-decf eglot--buffer-open-count) - (when (< eglot--buffer-open-count 0) - (error "Too many textDocument/didClose notifs for %s" (current-buffer))) - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didClose - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) + (let* ((proc (eglot--current-process-or-lose)) + (count (1- (or (gethash (current-buffer) + (eglot--buffer-open-count proc)) + 0)))) + (when (< count 0) + (eglot--error "Too many textDocument/didClose notifs for %s" (current-buffer))) + (setf (gethash (current-buffer) (eglot--buffer-open-count proc)) + count) + (eglot--notify proc + :textDocument/didClose + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem))))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." From 5a3d92cab383d385586fc3afb074b40469e81205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 11:21:11 +0100 Subject: [PATCH 057/771] Connect to lsp server via tcp * eglot.el (eglot--make-process): Rename from eglot-make-local-process. (eglot): Fix docstring and rework. (eglot--bootstrap-fn): Remove (eglot--contact): New process-local var. (eglot--connect): Take CONTACT arg. (eglot--reconnect): Rework. --- lisp/progmodes/eglot.el | 137 +++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 64 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ac52b9adab9..c5a0c0f0d7a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -133,30 +133,40 @@ A list (ID WHAT DONE-P)." t) "Status as declared by the server. A list (WHAT SERIOUS-P)." t) -(eglot--define-process-var eglot--bootstrap-fn nil - "Function for returning processes/connetions to LSP servers. -Must be a function of one arg, a name, returning a process -object.") +(eglot--define-process-var eglot--contact nil + "Method used to contact a server. +Either a list of strings (a shell command and arguments), or a +list of a single string of the form :") (eglot--define-process-var eglot--buffer-open-count (make-hash-table) "Keeps track of didOpen/didClose notifs for each buffer.") -(defun eglot-make-local-process (name command) - "Make a local LSP process from COMMAND. +(defun eglot--make-process (name contact) + "Make a process from CONTACT. NAME is a name to give the inferior process or connection. -Returns a process object." +CONTACT is as `eglot--contact'. Returns a process object." (let* ((readable-name (format "EGLOT server (%s)" name)) + (buffer (get-buffer-create + (format "*%s inferior*" readable-name))) + (singleton (and (null (cdr contact)) (car contact))) (proc - (make-process - :name readable-name - :buffer (get-buffer-create - (format "*%s inferior*" readable-name)) - :command command - :connection-type 'pipe - :filter 'eglot--process-filter - :sentinel 'eglot--process-sentinel - :stderr (get-buffer-create (format "*%s stderr*" - name))))) + (if (and + singleton + (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" + singleton)) + (open-network-stream readable-name + buffer + (match-string 1 singleton) + (string-to-number (match-string 2 singleton))) + (make-process + :name readable-name + :buffer buffer + :command contact + :connection-type 'pipe + :stderr (get-buffer-create (format "*%s stderr*" + name)))))) + (set-process-filter proc #'eglot--process-filter) + (set-process-sentinel proc #'eglot--process-sentinel) proc)) (defmacro eglot--obj (&rest what) @@ -180,13 +190,12 @@ Returns a process object." retval)) (defun eglot--connect (project managed-major-mode - short-name bootstrap-fn &optional success-fn) - "Make a connection for PROJECT, SHORT-NAME and MANAGED-MAJOR-MODE. -Use BOOTSTRAP-FN to make the actual process object. Call + short-name contact &optional success-fn) + "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. SUCCESS-FN with no args if all goes well." - (let* ((proc (funcall bootstrap-fn short-name)) + (let* ((proc (eglot--make-process short-name contact)) (buffer (process-buffer proc))) - (setf (eglot--bootstrap-fn proc) bootstrap-fn + (setf (eglot--contact proc) contact (eglot--project proc) project (eglot--major-mode proc) managed-major-mode) (with-current-buffer buffer @@ -226,15 +235,17 @@ SUCCESS-FN with no args if all goes well." "History of COMMAND arguments to `eglot'.") (defun eglot (managed-major-mode command &optional interactive) - ;; FIXME: Later make this function also connect to TCP servers by - ;; overloading semantics on COMMAND. "Start a Language Server Protocol server. Server is started with COMMAND and manages buffers of MANAGED-MAJOR-MODE for the current project. COMMAND is a list of strings, an executable program and -optionally its arguments. MANAGED-MAJOR-MODE is an Emacs major -mode. +optionally its arguments. If the first and only string in the +list is of the form \":\" it is taken as an +indication to connect to a server instead of starting one. This +is also know as the server's \"contact\". + +MANAGED-MAJOR-MODE is an Emacs major mode. With a prefix arg, prompt for MANAGED-MAJOR-MODE and COMMAND, else guess them from current context and `eglot-executables'. @@ -257,47 +268,45 @@ INTERACTIVE is t if called interactively." (cdr (assoc managed-major-mode eglot-executables)))) (list managed-major-mode - (if current-prefix-arg - (split-string-and-unquote - (read-shell-command "[eglot] Run program: " - (combine-and-quote-strings guessed-command) - 'eglot-command-history)) - guessed-command) + (let ((prompt + (cond (current-prefix-arg + "[eglot] Execute program (or connect to :) ") + ((null guessed-command) + (format "[eglot] Sorry, couldn't guess for `%s'!\n\ +Execute program (or connect to :) " + managed-major-mode))))) + (if prompt + (split-string-and-unquote + (read-shell-command prompt + (combine-and-quote-strings guessed-command) + 'eglot-command-history)) + guessed-command)) t))) (let* ((project (project-current)) (short-name (eglot--project-short-name project))) - (unless project (eglot--error - "Cannot work without a current project!")) - (let ((current-process (eglot--current-process)) - (command - (or command - (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)))) - (cond - ((and current-process - (process-live-p current-process)) - (when (and - interactive - (y-or-n-p "[eglot] Live process found, reconnect instead? ")) - (eglot-reconnect current-process interactive))) - (t - (eglot--connect - project - managed-major-mode - short-name - (lambda (name) - (eglot-make-local-process - name - command)) - (lambda (proc) - (eglot--message "Connected! Process `%s' now managing `%s'\ + (unless project (eglot--error "Cannot work without a current project!")) + (unless command (eglot--error "Don't know how to start EGLOT for %s buffers" + major-mode)) + (let ((current-process (eglot--current-process))) + (cond ((and (process-live-p current-process) + interactive + (y-or-n-p "[eglot] Live process found, reconnect instead? ")) + (eglot-reconnect current-process interactive)) + (t + (eglot--connect + project + managed-major-mode + short-name + command + (lambda (proc) + (eglot--message "Connected! Process `%s' now managing `%s'\ buffers in project %s." - proc - managed-major-mode - short-name) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc)))))))))) + proc + managed-major-mode + short-name) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc)))))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -309,7 +318,7 @@ INTERACTIVE is t if called interactively." (eglot--project process) (eglot--major-mode process) (eglot--short-name process) - (eglot--bootstrap-fn process) + (eglot--contact process) (lambda (proc) (eglot--message "Reconnected!") (dolist (buffer (buffer-list)) From c1e66cf87af0f2f26afbd55679bb68962fb76f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 01:25:13 +0100 Subject: [PATCH 058/771] When user declines to reconnect, first quit existing server * eglot.el (eglot): Rework reconnection logic. --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c5a0c0f0d7a..3d0c0448344 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -293,13 +293,15 @@ Execute program (or connect to :) " (y-or-n-p "[eglot] Live process found, reconnect instead? ")) (eglot-reconnect current-process interactive)) (t + (when (process-live-p current-process) + (eglot-shutdown current-process 'sync)) (eglot--connect project managed-major-mode short-name command (lambda (proc) - (eglot--message "Connected! Process `%s' now managing `%s'\ + (eglot--message "Connected! Process `%s' now managing `%s' \ buffers in project %s." proc managed-major-mode From f26ff4e8162d1798cf3c877f162086b31a33e9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 10:47:17 +0100 Subject: [PATCH 059/771] Make m-x eglot's interactive spec a separate function * eglot.el (eglot--interactive): New function. (eglot): Rework a little. --- lisp/progmodes/eglot.el | 104 ++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d0c0448344..ec7f3e6653c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -234,6 +234,39 @@ SUCCESS-FN with no args if all goes well." (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") +(defun eglot--interactive () + "Helper for `eglot'." + (let* ((managed-major-mode + (cond + ((or current-prefix-arg + (not buffer-file-name)) + (intern + (completing-read + "[eglot] Start a server to manage buffers of what major mode? " + (mapcar #'symbol-name + (eglot--all-major-modes)) nil t + (symbol-name major-mode) nil + (symbol-name major-mode) nil))) + (t major-mode))) + (guessed-command + (cdr (assoc managed-major-mode eglot-executables)))) + (list + managed-major-mode + (let ((prompt + (cond (current-prefix-arg + "[eglot] Execute program (or connect to :) ") + ((null guessed-command) + (format "[eglot] Sorry, couldn't guess for `%s'!\n\ +Execute program (or connect to :) " + managed-major-mode))))) + (if prompt + (split-string-and-unquote + (read-shell-command prompt + (combine-and-quote-strings guessed-command) + 'eglot-command-history)) + guessed-command)) + t))) + (defun eglot (managed-major-mode command &optional interactive) "Start a Language Server Protocol server. Server is started with COMMAND and manages buffers of @@ -251,64 +284,33 @@ With a prefix arg, prompt for MANAGED-MAJOR-MODE and COMMAND, else guess them from current context and `eglot-executables'. INTERACTIVE is t if called interactively." - (interactive - (let* ((managed-major-mode - (cond - ((or current-prefix-arg - (not buffer-file-name)) - (intern - (completing-read - "[eglot] Start a server to manage buffers of what major mode? " - (mapcar #'symbol-name - (eglot--all-major-modes)) nil t - (symbol-name major-mode) nil - (symbol-name major-mode) nil))) - (t major-mode))) - (guessed-command - (cdr (assoc managed-major-mode eglot-executables)))) - (list - managed-major-mode - (let ((prompt - (cond (current-prefix-arg - "[eglot] Execute program (or connect to :) ") - ((null guessed-command) - (format "[eglot] Sorry, couldn't guess for `%s'!\n\ -Execute program (or connect to :) " - managed-major-mode))))) - (if prompt - (split-string-and-unquote - (read-shell-command prompt - (combine-and-quote-strings guessed-command) - 'eglot-command-history)) - guessed-command)) - t))) + (interactive (eglot--interactive)) (let* ((project (project-current)) (short-name (eglot--project-short-name project))) (unless project (eglot--error "Cannot work without a current project!")) (unless command (eglot--error "Don't know how to start EGLOT for %s buffers" major-mode)) (let ((current-process (eglot--current-process))) - (cond ((and (process-live-p current-process) - interactive - (y-or-n-p "[eglot] Live process found, reconnect instead? ")) - (eglot-reconnect current-process interactive)) - (t - (when (process-live-p current-process) - (eglot-shutdown current-process 'sync)) - (eglot--connect - project - managed-major-mode - short-name - command - (lambda (proc) - (eglot--message "Connected! Process `%s' now managing `%s' \ + (if (and (process-live-p current-process) + interactive + (y-or-n-p "[eglot] Live process found, reconnect instead? ")) + (eglot-reconnect current-process interactive) + (when (process-live-p current-process) + (eglot-shutdown current-process 'sync)) + (eglot--connect + project + managed-major-mode + short-name + command + (lambda (proc) + (eglot--message "Connected! Process `%s' now managing `%s' \ buffers in project %s." - proc - managed-major-mode - short-name) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc)))))))))) + proc + managed-major-mode + short-name) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc))))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. From 6b01d54a76518bc567c8f714c105347cd0f6449c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 10:49:34 +0100 Subject: [PATCH 060/771] Eglot-editing-mode becomes eglot--managed-mode * eglot.el (eglot--sentinel): Use eglot--managed-mode. (eglot--managed-mode-map): Renamed from eglot-editing-mode-map. (eglot--managed-mode): Renamed from eglot-editing-mode. (eglot-mode): Simplify. (eglot--buffer-managed-p): New function. (eglot--maybe-activate-editing-mode): Simplify. --- lisp/progmodes/eglot.el | 63 +++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ec7f3e6653c..0300ad3da09 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -341,12 +341,20 @@ INTERACTIVE is t if called interactively." "(sentinel) Cancelling timer for continuation %s" id) (cancel-timer timeout))) (eglot--pending-continuations process)) + ;; Turn off `eglot--managed-mode' where appropriate. + ;; + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (eglot--buffer-managed-p process) + (eglot--managed-mode -1)))) + ;; Forget about the process-project relationship + ;; + (setf (gethash (eglot--project process) eglot--processes-by-project) + (delq process + (gethash (eglot--project process) eglot--processes-by-project))) (cond ((eglot--moribund process) (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status process)) - (setf (gethash (eglot--project process) eglot--processes-by-project) - (delq process - (gethash (eglot--project process) eglot--processes-by-project)))) + (process-exit-status process))) (t (eglot--warn "(sentinel) Reconnecting after process unexpectedly changed to %s." @@ -641,60 +649,55 @@ identifier. ERROR is non-nil if this is an error." ;;; (defvar eglot-mode-map (make-sparse-keymap)) -(defvar eglot-editing-mode-map (make-sparse-keymap)) +(defvar eglot--managed-mode-map (make-sparse-keymap)) -(define-minor-mode eglot-editing-mode - "Minor mode for source buffers where EGLOT helps you edit." +(define-minor-mode eglot--managed-mode + "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map (cond - (eglot-editing-mode + (eglot--managed-mode (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) - (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) + ;; (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) - (flymake-mode 1) - (unless (eglot--current-process) - (eglot--warn "No process, start one with `M-x eglot'"))) + (flymake-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) - (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) + ;; (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil - nil eglot-mode-map - (cond (eglot-mode - (when (and buffer-file-name - (not eglot-editing-mode)) - (eglot-editing-mode 1))) - (t - (when eglot-editing-mode - (eglot-editing-mode -1))))) + nil eglot-mode-map) + +(defun eglot--buffer-managed-p (&optional proc) + "Tell if current buffer is managed by PROC." + (and buffer-file-name + (let ((cur (eglot--current-process))) + (or (and (null proc) cur) + (and proc (eq proc cur)))))) (defun eglot--maybe-activate-editing-mode (&optional proc) - "Maybe activate mode function `eglot-editing-mode'. + "Maybe activate mode function `eglot--managed-mode'. If PROC is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." - (when buffer-file-name - (let ((cur (eglot--current-process))) - (when (or (and (null proc) cur) - (and proc (eq proc cur))) - (unless eglot-editing-mode - (eglot-editing-mode 1)) - (eglot--signal-textDocument/didOpen) - (flymake-start))))) + ;; Called even when revert-buffer-in-progress-p + (when (eglot--buffer-managed-p proc) + (eglot--managed-mode 1) + (eglot--signal-textDocument/didOpen) + (flymake-start))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) From fd9792fcc7896f150a851814b348ff30f64bce19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 11:54:06 +0100 Subject: [PATCH 061/771] Workaround rls's regusal to treat nil as empty json object * eglot.el (eglot--connect): Use dummy params. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0300ad3da09..8f371d2011b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -229,7 +229,7 @@ SUCCESS-FN with no args if all goes well." (setf (eglot--capabilities proc) capabilities) (setf (eglot--status proc) nil) (when success-fn (funcall success-fn proc)) - (eglot--notify proc :initialized nil)))))))) + (eglot--notify proc :initialized (eglot--obj :__dummy__ t))))))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") From 4f62e731d8e1b4c30bff9f24dd4020cb2a11b06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 12:03:51 +0100 Subject: [PATCH 062/771] Don't auto-reconnect if last attempt lasted less than 3 seconds * eglot.el (eglot--inhibit-auto-reconnect): New var. (eglot--process-sentinel): Use it. --- lisp/progmodes/eglot.el | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8f371d2011b..df03d044789 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -329,6 +329,9 @@ INTERACTIVE is t if called interactively." (with-current-buffer buffer (eglot--maybe-activate-editing-mode proc)))))) +(defvar eglot--inhibit-auto-reconnect nil + "If non-nil, don't autoreconnect on unexpected quit.") + (defun eglot--process-sentinel (process change) "Called with PROCESS undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) @@ -355,11 +358,20 @@ INTERACTIVE is t if called interactively." (cond ((eglot--moribund process) (eglot--message "(sentinel) Moribund process exited with status %s" (process-exit-status process))) - (t + ((null eglot--inhibit-auto-reconnect) (eglot--warn "(sentinel) Reconnecting after process unexpectedly changed to %s." change) - (eglot-reconnect process))) + (eglot-reconnect process) + (setq eglot--inhibit-auto-reconnect + (run-with-timer + 3 nil + (lambda () + (setq eglot--inhibit-auto-reconnect nil))))) + (t + (eglot--warn + "(sentinel) Not auto-reconnecting, last one didn't last long." + change))) (force-mode-line-update t) (delete-process process))) From 70fc9cc98d057cfc5e8bd35c60699edd2fb5107d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 12:17:26 +0100 Subject: [PATCH 063/771] Handle requests from server correctly * eglot.el (eglot--process-receive): Redesign. (eglot--process-send): Take REPLY arg. Discover if message is error. (eglot--reply): new function (eglot--log-event): Tweak docstring. (eglot--process-receive): Reply with -32601 if unimplemented. (eglot--server-window/showMessageRequest) (eglot--server-client/registerCapability): Use eglot--reply --- lisp/progmodes/eglot.el | 95 ++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index df03d044789..332f1fe849e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -460,7 +460,7 @@ INTERACTIVE is t if called interactively." "Log an eglot-related event. PROC is the current process. TYPE is an identifier. MESSAGE is a JSON-like plist or anything else. ID is a continuation -identifier. ERROR is non-nil if this is an error." +identifier. ERROR is non-nil if this is a JSON-RPC error." (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) (goto-char (point-max)) @@ -475,53 +475,49 @@ identifier. ERROR is non-nil if this is an error." (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." - (let* ((response-id (plist-get message :id)) + (let* ((id (plist-get message :id)) + (method (plist-get message :method)) (err (plist-get message :error)) - (continuations (and response-id - (gethash response-id - (eglot--pending-continuations proc))))) + (continuations (and id + (not method) + (gethash id (eglot--pending-continuations proc))))) (eglot--log-event proc - (cond ((not response-id) - 'server-notification) - ((not continuations) - 'unexpected-server-reply) - (t - 'server-reply)) + (cond ((and method id) 'server-request) + (method 'server-notification) + (continuations 'server-reply) + ;; pyls keeps on sending these + (t 'unexpected-server-thingy)) message - response-id + id err) - (when err - (setf (eglot--status proc) '("error" t))) - (cond ((and response-id - (not continuations)) - (eglot--warn "Ooops no continuation for id %s" response-id)) - (continuations - (cancel-timer (cl-third continuations)) - (remhash response-id - (eglot--pending-continuations proc)) - (cond (err - (apply (cl-second continuations) err)) - (t - (apply (cl-first continuations) (plist-get message :result))))) - (t + (when err (setf (eglot--status proc) '("error" t))) + (cond (method ;; a server notification or a server request - (let* ((method (plist-get message :method)) - (handler-sym (intern (concat "eglot--server-" + (let* ((handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) (apply handler-sym proc (append (plist-get message :params) - (let ((id (plist-get message :id))) - (if id `(:id ,id))))) - ;; pyls keeps on sending nil notifs for each notif we - ;; send it, just ignore these. - (unless (null method) - (eglot--warn "No implemetation for notification %s yet" - method)))))))) + (if id `(:id ,id)))) + (eglot--warn "No implementation of method %s yet" + method) + (when id + (eglot--reply + proc id + :error (eglot--obj :code -32601 + :message "Method unimplemented")))))) + (continuations + (cancel-timer (cl-third continuations)) + (remhash id (eglot--pending-continuations proc)) + (if err + (apply (cl-second continuations) err) + (apply (cl-first continuations) (plist-get message :result)))) + (id + (eglot--warn "Ooops no continuation for id %s" id))))) (defvar eglot--expect-carriage-return nil) -(defun eglot--process-send (id proc message) +(defun eglot--process-send (id proc message &optional reply) "Send MESSAGE to PROC (ID is optional)." (let* ((json (json-encode message)) (to-send (format "Content-Length: %d\r\n\r\n%s" @@ -529,9 +525,11 @@ identifier. ERROR is non-nil if this is an error." json))) (process-send-string proc to-send) (eglot--log-event proc (if id - 'client-request + (if reply + 'client-reply + 'client-request) 'client-notification) - message id nil))) + message id (plist-get message :error)))) (defvar eglot--next-request-id 0) @@ -627,6 +625,15 @@ identifier. ERROR is non-nil if this is an error." :method method :params params))) +(cl-defun eglot--reply (process id &key result error) + "Reply to PROCESS's request ID with MESSAGE." + (eglot--process-send id process + (eglot--obj :jsonrpc "2.0" + :id nil + :result result + :error error) + t)) + ;;; Helpers ;;; @@ -884,13 +891,11 @@ running. INTERACTIVE is t if called interactively." nil t (plist-get (elt actions 0) :title))) - (eglot--process-send - id - process - (if reply - (eglot--obj :result (eglot--obj :title reply)) - ;; request cancelled - (eglot--obj :error -32800)))))) + (if reply + (eglot--reply process id :result (eglot--obj :title reply)) + (eglot--reply process id + :error (eglot--obj :code -32800 + :message "User cancelled")))))) (cl-defun eglot--server-window/logMessage (_process &key type message) From 20e044b1a1fe1318110f5dec0f633397bd0f51cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 13:36:33 +0100 Subject: [PATCH 064/771] Reply to client/registercapability (don't handle it yet) * eglot.el (eglot--server-client/registerCapability): New function. --- lisp/progmodes/eglot.el | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 332f1fe849e..d073be2603e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -962,6 +962,19 @@ running. INTERACTIVE is t if called interactively." (t (eglot--message "OK so %s isn't visited" filename))))) +(cl-defun eglot--server-client/registerCapability + (proc &key id registrations) + "Handle notification client/registerCapability" + (mapc (lambda (reg) + (apply (cl-function + (lambda (&key _id _method _registerOptions) + ;;; TODO: handle this + )) + reg)) + registrations) + (eglot--reply proc id :error (eglot--obj :code -32601 + :message "sorry :-("))) + (defvar eglot--recent-before-changes nil "List of recent changes as collected by `eglot--before-change'.") (defvar eglot--recent-after-changes nil From 4b678a2fa9de46793a7ee802a19a7592db0a903c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 14:15:41 +0100 Subject: [PATCH 065/771] Include source info in diagnostics * eglot.el (eglot--server-textDocument/publishDiagnostics): Include source info. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d073be2603e..10435a16f8a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -936,7 +936,7 @@ running. INTERACTIVE is t if called interactively." (point)))) (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range severity _group - _code _source message) + _code source message) diag-spec (cl-destructuring-bind (&key start end) range @@ -951,7 +951,7 @@ running. INTERACTIVE is t if called interactively." :warning) (t :note)) - message)))) + (concat source ": " message))))) into diags finally (if eglot--current-flymake-report-fn From adbed0c21ae0e06c374a035c7f424d8e93b1e70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 14:18:12 +0100 Subject: [PATCH 066/771] Make reported capabilities into its own function * eglot.el (eglot--client-capabilities): New function. (eglot--connect): Use it. --- lisp/progmodes/eglot.el | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 10435a16f8a..b305ca303c3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -189,6 +189,13 @@ CONTACT is as `eglot--contact'. Returns a process object." (push sym retval)))) retval)) +(defun eglot--client-capabilities () + "What the EGLOT LSP client supports." + (eglot--obj + :workspace (eglot--obj) + :textDocument (eglot--obj + :publishDiagnostics `(:relatedInformation nil)))) + (defun eglot--connect (project managed-major-mode short-name contact &optional success-fn) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. @@ -218,11 +225,7 @@ SUCCESS-FN with no args if all goes well." (expand-file-name (car (project-roots (project-current))))) :initializationOptions [] - :capabilities - (eglot--obj - :workspace (eglot--obj) - :textDocument (eglot--obj - :publishDiagnostics `(:relatedInformation t)))) + :capabilities (eglot--client-capabilities)) :success-fn (cl-function (lambda (&key capabilities) From 42733a1e9fd0501217857f98560ddbbe461aa0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 14:29:40 +0100 Subject: [PATCH 067/771] Use rooturi instead of rootpath * eglot.el (eglot--connect) (eglot--current-buffer-VersionedTextDocumentIdentifier): Use eglot--uri. (eglot--uri): New function. --- lisp/progmodes/eglot.el | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b305ca303c3..8e39591a70d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -221,9 +221,9 @@ SUCCESS-FN with no args if all goes well." proc :initialize (eglot--obj :processId (emacs-pid) - :rootPath (concat - (expand-file-name (car (project-roots - (project-current))))) + :rootUri (eglot--uri + (expand-file-name (car (project-roots + (project-current))))) :initializationOptions [] :capabilities (eglot--client-capabilities)) :success-fn @@ -666,6 +666,8 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (apply #'format format args) :warning))) +(defun eglot--uri (path) "Add file:// to PATH." (concat "file://" path)) + ;;; Minor modes ;;; @@ -988,10 +990,10 @@ running. INTERACTIVE is t if called interactively." (defun eglot--current-buffer-VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (eglot--obj :uri - (concat "file://" - (url-hexify-string - (file-truename buffer-file-name) - url-path-allowed-chars)) + (eglot--uri + (url-hexify-string + (file-truename buffer-file-name) + url-path-allowed-chars)) ;; FIXME: later deal with workspaces :version eglot--versioned-identifier)) From 8e4db752095664122ea1385ad64abd1dc7546fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 14:42:02 +0100 Subject: [PATCH 068/771] Be quite explicit about our lack of capabilities right now * eglot.el (eglot--client-capabilities): Spread out. --- lisp/progmodes/eglot.el | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8e39591a70d..57014222940 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -192,9 +192,40 @@ CONTACT is as `eglot--contact'. Returns a process object." (defun eglot--client-capabilities () "What the EGLOT LSP client supports." (eglot--obj - :workspace (eglot--obj) + :workspace (eglot--obj + :applyEdit nil + :workspaceEdit nil + :didChangeConfiguration nil + :didChangeWatchedFiles nil + :symbol nil + :executeCommand nil + :workspaceFolders nil + :configuration nil) :textDocument (eglot--obj - :publishDiagnostics `(:relatedInformation nil)))) + :synchronization (eglot--obj + :dynamicRegistration :json-false + :willSave t + :willSaveWaitUntil :json-false + :didSave t) + :completion nil + :hover nil + :signatureHelp nil + :references nil + :documentHighlight nil + :documentSymbol nil + :formatting nil + :rangeFormatting nil + :onTypeFormatting nil + :definition nil + :typeDefinition nil + :implementation nil + :codeAction nil + :codeLens nil + :documentLink nil + :colorProvider nil + :rename nil + :publishDiagnostics `(:relatedInformation :json-false)) + :experimental (eglot--obj))) (defun eglot--connect (project managed-major-mode short-name contact &optional success-fn) From 1e893ab726de8eea25655d714ea28cedd984de4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 15:02:03 +0100 Subject: [PATCH 069/771] Honour textdocumentsync * eglot.el (eglot--signal-textDocument/didChange): Honour textDocumentSync --- lisp/progmodes/eglot.el | 72 +++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 57014222940..68fe3e287a8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -251,7 +251,9 @@ SUCCESS-FN with no args if all goes well." (eglot--request proc :initialize - (eglot--obj :processId (emacs-pid) + (eglot--obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) :rootUri (eglot--uri (expand-file-name (car (project-roots (project-current))))) @@ -1070,44 +1072,44 @@ Records START, END and PRE-CHANGE-LENGTH locally." "Send textDocument/didChange to server." (when (and eglot--recent-before-changes eglot--recent-after-changes) - (save-excursion + (let* ((proc (eglot--current-process-or-lose)) + (sync-kind (plist-get (eglot--capabilities proc) :textDocumentSync))) (save-restriction (widen) - (if (/= (length eglot--recent-before-changes) - (length eglot--recent-after-changes)) - (eglot--notify - (eglot--current-process-or-lose) - :textDocument/didChange - (eglot--obj - :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges - (vector - (eglot--obj - :text (buffer-substring-no-properties (point-min) (point-max)))))) - (let ((combined (cl-mapcar 'append - eglot--recent-before-changes - eglot--recent-after-changes))) - (eglot--notify - (eglot--current-process-or-lose) - :textDocument/didChange - (eglot--obj - :textDocument (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges + (unless (or (not sync-kind) + (eq sync-kind 0)) + (eglot--notify + proc + :textDocument/didChange + (eglot--obj + :textDocument + (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (if (or (eq sync-kind 1) + (/= (length eglot--recent-before-changes) + (length eglot--recent-after-changes))) + (vector + (eglot--obj + :text (buffer-substring-no-properties (point-min) (point-max)))) (apply #'vector - (mapcar (pcase-lambda (`(,before-start-position - ,before-end-position - ,after-start - ,after-end - ,len)) - (eglot--obj - :range - (eglot--obj - :start before-start-position - :end before-end-position) - :rangeLength len - :text (buffer-substring-no-properties after-start after-end))) - (reverse combined)))))))))) + (mapcar + (pcase-lambda (`(,before-start-position + ,before-end-position + ,after-start + ,after-end + ,len)) + (eglot--obj + :range + (eglot--obj + :start before-start-position + :end before-end-position) + :rangeLength len + :text (buffer-substring-no-properties after-start after-end))) + (reverse + (cl-mapcar 'append + eglot--recent-before-changes + eglot--recent-after-changes))))))))))) (setq eglot--recent-before-changes nil eglot--recent-after-changes nil)) From b83cd67751ecb8a1d08993f0751bc5d3dc480a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 4 May 2018 15:19:19 +0100 Subject: [PATCH 070/771] Handle dynamic registration in general (but nothing specific yet) * eglot.el (eglot--server-client/registerCapability): Implement. (eglot--register-workspace/didChangeWatchedFiles): Dummy registrator. --- lisp/progmodes/eglot.el | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 68fe3e287a8..2b3f270ef9c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1003,15 +1003,28 @@ running. INTERACTIVE is t if called interactively." (cl-defun eglot--server-client/registerCapability (proc &key id registrations) "Handle notification client/registerCapability" - (mapc (lambda (reg) - (apply (cl-function - (lambda (&key _id _method _registerOptions) - ;;; TODO: handle this - )) - reg)) - registrations) - (eglot--reply proc id :error (eglot--obj :code -32601 - :message "sorry :-("))) + (catch 'done + (mapc + (lambda (reg) + (apply + (cl-function + (lambda (&key id method registerOptions) + (pcase-let* + ((handler-sym (intern (concat "eglot--register-" + method))) + (`(,ok ,message) + (and (functionp handler-sym) + (apply handler-sym proc :id id registerOptions)))) + (unless ok + (throw + 'done + (eglot--reply proc id + :error (eglot--obj + :code -32601 + :message (or message "sorry :-(")))))))) + reg)) + registrations) + (eglot--reply proc id :result (eglot--obj :message "OK")))) (defvar eglot--recent-before-changes nil "List of recent changes as collected by `eglot--before-change'.") @@ -1176,6 +1189,16 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;; make the server report new diagnostics. (eglot--signal-textDocument/didChange)) + +;;; Dynamic registration +;;; +(cl-defun eglot--register-workspace/didChangeWatchedFiles + (_proc &key _id _watchers) + "Handle dynamic registration of workspace/didChangeWatchedFiles" + ;; TODO: file-notify-add-watch and + ;; file-notify-rm-watch can probably handle this + (list nil "Sorry, can't do this yet")) + ;;; Rust-specific ;;; From 632a39624d5fcb334a34cc64e374d6aa86e1c03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 May 2018 02:24:37 +0100 Subject: [PATCH 071/771] Cleanup mistake with textdocumentitem and textdocumentidentifier Also introduce eglot--path-to-uri * eglot.el (eglot--path-to-uri): Rename from eglot--uri and rework. (eglot--connect): Use it. (eglot--current-buffer-TextDocumentIdentifier): New function. (eglot--current-buffer-VersionedTextDocumentIdentifier) (eglot--signal-textDocument/didChange) (eglot--signal-textDocument/didClose) (eglot--signal-textDocument/willSave) (eglot--signal-textDocument/didSave): Use it. --- lisp/progmodes/eglot.el | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2b3f270ef9c..2afb0e7556d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -254,9 +254,8 @@ SUCCESS-FN with no args if all goes well." (eglot--obj :processId (unless (eq (process-type proc) 'network) (emacs-pid)) - :rootUri (eglot--uri - (expand-file-name (car (project-roots - (project-current))))) + :rootUri (eglot--path-to-uri + (car (project-roots (project-current)))) :initializationOptions [] :capabilities (eglot--client-capabilities)) :success-fn @@ -699,7 +698,11 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (apply #'format format args) :warning))) -(defun eglot--uri (path) "Add file:// to PATH." (concat "file://" path)) +(defun eglot--path-to-uri (path) + "Urify PATH." + (url-hexify-string + (concat "file://" (file-truename path)) + url-path-allowed-chars)) ;;; Minor modes @@ -1033,15 +1036,14 @@ running. INTERACTIVE is t if called interactively." (defvar-local eglot--versioned-identifier 0) +(defun eglot--current-buffer-TextDocumentIdentifier () + "Compute TextDocumentIdentifier object for current buffer." + (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) + (defun eglot--current-buffer-VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." - (eglot--obj :uri - (eglot--uri - (url-hexify-string - (file-truename buffer-file-name) - url-path-allowed-chars)) - ;; FIXME: later deal with workspaces - :version eglot--versioned-identifier)) + (append (eglot--current-buffer-TextDocumentIdentifier) + (eglot--obj :version eglot--versioned-identifier))) (defun eglot--current-buffer-TextDocumentItem () "Compute TextDocumentItem object for current buffer." @@ -1154,7 +1156,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--notify proc :textDocument/didClose (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem))))) + (eglot--current-buffer-TextDocumentIdentifier))))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." @@ -1163,7 +1165,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." :textDocument/willSave (eglot--obj :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha - :textDocument (eglot--current-buffer-TextDocumentItem)))) + :textDocument (eglot--current-buffer-TextDocumentIdentifier)))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." @@ -1173,7 +1175,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) - :textDocument (eglot--current-buffer-TextDocumentItem)))) + :textDocument (eglot--current-buffer-TextDocumentIdentifier)))) (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. From a2c01431d8893bed71e88bc4d38bcaafc262ba13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 May 2018 02:16:10 +0100 Subject: [PATCH 072/771] New helper eglot--sync-request This should help with xref definitions * eglot.el (eglot--request): Rework a bit. Continuation is always cleared on timeout, regardless of user-supplied fn. (eglot--sync-request): New function. (eglot--process-receive): watch out for vector results. --- lisp/progmodes/eglot.el | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2afb0e7556d..34ce0f80f4e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -546,7 +546,10 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (remhash id (eglot--pending-continuations proc)) (if err (apply (cl-second continuations) err) - (apply (cl-first continuations) (plist-get message :result)))) + (let ((res (plist-get message :result))) + (if (listp res) + (apply (cl-first continuations) res) + (funcall (cl-first continuations) res))))) (id (eglot--warn "Ooops no continuation for id %s" id))))) @@ -592,8 +595,7 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (or timeout-fn (lambda () (eglot--warn - "(request) Tired of waiting for reply to %s" id) - (remhash id (eglot--pending-continuations process))))) + "(request) Tired of waiting for reply to %s" id)))) (error-fn (or error-fn (cl-function @@ -618,11 +620,15 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." :params params)) (catch catch-tag (let ((timeout-timer - (run-with-timer 5 nil - (if async-p - timeout-fn - (lambda () - (throw catch-tag (funcall timeout-fn))))))) + (run-with-timer + 5 nil + (if async-p + (lambda () + (remhash id (eglot--pending-continuations process)) + (funcall timeout-fn)) + (lambda () + (remhash id (eglot--pending-continuations process)) + (throw catch-tag (funcall timeout-fn))))))) (puthash id (list (if async-p success-fn @@ -651,6 +657,23 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." "(request) Last-change cancelling timer for continuation %s" id) (cancel-timer timeout-timer)))))))) +(defun eglot--sync-request (proc method params) + "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. +Meaning only return locally if successful, otherwise exit non-locally." + (eglot--request proc method params + :success-fn (lambda (&rest args) + (if (vectorp (car args)) + (car args) + args)) + :error-fn (cl-function + (lambda (&key code message &allow-other-keys) + (eglot--error "Oops: %s: %s" code message))) + :timeout-fn (lambda () + (lambda () + (eglot--error + "Tired of waiting for reply to sync request"))) + :async-p nil)) + (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" (eglot--process-send nil From bdd5f6961839c073713d6c2a184260acbe5696ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 May 2018 02:29:06 +0100 Subject: [PATCH 073/771] Very basic xref support * eglot.el (eglot--pos-to-lisp-position): Move up. (eglot--mapply, eglot--lambda): New helpers. (eglot--uri-to-path): New helper. (eglot--managed-mode): Manage xref-backend-functions. (eglot-xref-backend): New function. (xref-backend-identifier-completion-table) (xref-backend-identifier-at-point) (xref-backend-definitions): New methods. (xref-backend-references) (xref-backend-apropos): New methods, still unimplemented. --- lisp/progmodes/eglot.el | 93 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 34ce0f80f4e..ad9ad52ba8b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -721,12 +721,36 @@ Meaning only return locally if successful, otherwise exit non-locally." (apply #'format format args) :warning))) +(defun eglot--pos-to-lsp-position (&optional pos) + "Convert point POS to LSP position." + (save-excursion + (eglot--obj :line + ;; F!@(#*&#$)CKING OFF-BY-ONE + (1- (line-number-at-pos pos t)) + :character + (- (goto-char (or pos (point))) + (line-beginning-position))))) + +(defun eglot--mapply (fun seq) + "Apply FUN to every element of SEQ." + (mapcar (lambda (e) (apply fun e)) seq)) + +(cl-defmacro eglot--lambda (cl-lambda-list &body body) + (declare (indent 1)) + `(cl-function + (lambda ,cl-lambda-list + ,@body))) + (defun eglot--path-to-uri (path) "Urify PATH." (url-hexify-string (concat "file://" (file-truename path)) url-path-allowed-chars)) +(defun eglot--uri-to-path (uri) + "Convert URI to a file path." + (url-filename (url-generic-parse-url (url-unhex-string uri)))) + ;;; Minor modes ;;; @@ -750,6 +774,7 @@ Meaning only return locally if successful, otherwise exit non-locally." ;; (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) + (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (flymake-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) @@ -759,7 +784,8 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) ;; (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) - (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t)))) + (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) + (remove-hook 'xref-backend-functions 'eglot-xref-backend t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -1080,16 +1106,6 @@ running. INTERACTIVE is t if called interactively." (widen) (buffer-substring-no-properties (point-min) (point-max)))))) -(defun eglot--pos-to-lsp-position (pos) - "Convert point POS to LSP position." - (save-excursion - (eglot--obj :line - ;; F!@(#*&#$)CKING OFF-BY-ONE - (1- (line-number-at-pos pos t)) - :character - (- (goto-char pos) - (line-beginning-position))))) - (defun eglot--before-change (start end) "Hook onto `before-change-functions'. Records START and END, crucially convert them into @@ -1214,6 +1230,61 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;; make the server report new diagnostics. (eglot--signal-textDocument/didChange)) +(defun eglot-xref-backend () "EGLOT xref backend." 'eglot) + +(cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) + (eglot--mapply + (eglot--lambda (&key name _kind _location _containerName) + ;; a shame we have to throw all that good stuff away + name) + (eglot--sync-request + (eglot--current-process-or-lose) + :textDocument/documentSymbol + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier))))) + +(cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) + (let ((symatpt (symbol-at-point))) + (when symatpt + (propertize (symbol-name symatpt) + :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position))))) + +(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) + (eglot--mapply + (eglot--lambda (&key uri range) + (xref-make identifier + (xref-make-file-location + (eglot--uri-to-path uri) + (plist-get (plist-get range :start) :line) + (plist-get (plist-get range :start) :character)))) + (or + ;; `identifier' already has `:locations' property if it was + ;; computed via `xref-backend-identifier-completion-table'... + ;; + (get-text-property 0 :locations identifier) + ;; otherwise, it came from + ;; `xref-backend-identifier-at-point', and we have to fetch + ;; manually + ;; + (let ((location-or-locations + (eglot--sync-request (eglot--current-process-or-lose) + :textDocument/definition + (eglot--obj + :textDocument + (get-text-property 0 :textDocument identifier) + :position + (get-text-property 0 :position identifier))))) + (if (vectorp (car location-or-locations)) + (car location-or-locations) + location-or-locations))))) + +(cl-defmethod xref-backend-references ((_backend (eql eglot)) _identifier) + (error "Not implemented")) + +(cl-defmethod xref-backend-apropos ((_backend (eql eglot)) _identifier) + (error "Not implemented")) + ;;; Dynamic registration ;;; From f23a8e8486ad8bbe5f1743571106673fd9d25140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 May 2018 11:26:12 +0100 Subject: [PATCH 074/771] Half-decent xref support * eglot.el (eglot--xref-known-symbols): New hacky var. (eglot--xref-reset-known-symbols): New helper. (xref-find-definitions, xref-find-references): Advise after to call the new helper. (xref-backend-identifier-completion-table): Rework. (eglot--xref-make): New helper. (xref-backend-definitions): Use it. (xref-backend-references, xref-backend-apropos): Implement. (eglot--obj): Add a debug spec. (eglot--lambda): Add debug spec. --- lisp/progmodes/eglot.el | 132 +++++++++++++++++++++++++++------------- 1 file changed, 90 insertions(+), 42 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ad9ad52ba8b..cb92361eecd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -33,6 +33,7 @@ (require 'compile) ; for some faces (require 'warnings) (require 'flymake) +(require 'xref) ;;; User tweakable stuff @@ -171,6 +172,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (defmacro eglot--obj (&rest what) "Make WHAT a suitable argument for `json-encode'." + (declare (debug (&rest form))) ;; FIXME: maybe later actually do something, for now this just fixes ;; the indenting of literal plists. `(list ,@what)) @@ -736,7 +738,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (mapcar (lambda (e) (apply fun e)) seq)) (cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1)) + (declare (indent 1) (debug (sexp &rest form))) `(cl-function (lambda ,cl-lambda-list ,@body))) @@ -1232,16 +1234,46 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (defun eglot-xref-backend () "EGLOT xref backend." 'eglot) +(defvar eglot--xref-known-symbols nil) + +(defun eglot--xref-reset-known-symbols () + "Reset `eglot--xref-reset-known-symbols'." + (setq eglot--xref-known-symbols nil)) + +(advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) +(advice-add 'xref-find-references :after #'eglot--xref-reset-known-symbols) + +(defun eglot--xref-make (name uri position) + "Like `xref-make' but with LSP's NAME, URI and POSITION." + (xref-make name + (xref-make-file-location + (eglot--uri-to-path uri) + ;; F!@(#*&#$)CKING OFF-BY-ONE again + (1+ (plist-get position :line)) + (plist-get position :character)))) + (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (eglot--mapply - (eglot--lambda (&key name _kind _location _containerName) - ;; a shame we have to throw all that good stuff away - name) - (eglot--sync-request - (eglot--current-process-or-lose) - :textDocument/documentSymbol - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier))))) + (let ((proc (eglot--current-process-or-lose)) + (text-id (eglot--current-buffer-TextDocumentIdentifier))) + (completion-table-with-cache + (lambda (string) + (setq eglot--xref-known-symbols + (eglot--mapply + (eglot--lambda (&key name kind location containerName) + (propertize name + :position (plist-get + (plist-get location :range) + :start) + :locations (list location) + :textDocument text-id + :kind kind + :containerName containerName)) + (eglot--sync-request + proc + :textDocument/documentSymbol + (eglot--obj + :textDocument text-id)))) + (all-completions string eglot--xref-known-symbols))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) (let ((symatpt (symbol-at-point))) @@ -1251,39 +1283,55 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." :position (eglot--pos-to-lsp-position))))) (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) + (let* ((rich-identifier + (car (member identifier eglot--xref-known-symbols))) + (location-or-locations + (if rich-identifier + (get-text-property 0 :locations rich-identifier) + (eglot--sync-request (eglot--current-process-or-lose) + :textDocument/definition + (eglot--obj + :textDocument + (get-text-property 0 :textDocument identifier) + :position + (get-text-property 0 :position identifier)))))) + (eglot--mapply + (eglot--lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + location-or-locations))) + +(cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) + (let* ((identifier (if (get-text-property 0 :position identifier) + identifier + (car (member identifier eglot--xref-known-symbols)))) + (position + (and identifier (get-text-property 0 :position identifier))) + (textDocument + (and identifier (get-text-property 0 :textDocument identifier)))) + (unless (and position textDocument) + (eglot--error "Sorry, can't discover where %s is in the workspace" + identifier)) + (eglot--mapply + (eglot--lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--sync-request (eglot--current-process-or-lose) + :textDocument/references + (eglot--obj + :textDocument + textDocument + :position + position + :context (eglot--obj :includeDeclaration t)))))) + +(cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (eglot--mapply - (eglot--lambda (&key uri range) - (xref-make identifier - (xref-make-file-location - (eglot--uri-to-path uri) - (plist-get (plist-get range :start) :line) - (plist-get (plist-get range :start) :character)))) - (or - ;; `identifier' already has `:locations' property if it was - ;; computed via `xref-backend-identifier-completion-table'... - ;; - (get-text-property 0 :locations identifier) - ;; otherwise, it came from - ;; `xref-backend-identifier-at-point', and we have to fetch - ;; manually - ;; - (let ((location-or-locations - (eglot--sync-request (eglot--current-process-or-lose) - :textDocument/definition - (eglot--obj - :textDocument - (get-text-property 0 :textDocument identifier) - :position - (get-text-property 0 :position identifier))))) - (if (vectorp (car location-or-locations)) - (car location-or-locations) - location-or-locations))))) - -(cl-defmethod xref-backend-references ((_backend (eql eglot)) _identifier) - (error "Not implemented")) - -(cl-defmethod xref-backend-apropos ((_backend (eql eglot)) _identifier) - (error "Not implemented")) + (eglot--lambda (&key name location &allow-other-keys) + (let ((range (plist-get location :range)) + (uri (plist-get location :uri))) + (eglot--xref-make name uri (plist-get range :start)))) + (eglot--sync-request (eglot--current-process-or-lose) + :workspace/symbol + (eglot--obj :query pattern)))) ;;; Dynamic registration From ace4b9150da111c721f46f29d2aab624b309d01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 11:53:44 +0100 Subject: [PATCH 075/771] Fix the odd bug here and there * eglot.el (eglot--connect): Activate editing mode where applicable here. (eglot, eglot-reconnect): Not here or here. (eglot--process-sentinel): Catch auto-reconnect errors. (eglot--notify): Dont send 'id=null', it messes up js's lsp (eglot--reply): Do send id here. (eglot--log-event): Simplify protocol. Complexify implementation. (eglot--process-receive, eglot--process-send): Simplify eglot--log-event call. (eglot--request, eglot--notify, eglot--reply): Simplify eglot--process-send call (eglot--server-client/registerCapability): Fix bug when replying with wrong id. (eglot--xref-reset-known-symbols): Take DUMMY arg. --- lisp/progmodes/eglot.el | 131 +++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 70 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cb92361eecd..d6ecdfed188 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -265,6 +265,9 @@ SUCCESS-FN with no args if all goes well." (lambda (&key capabilities) (setf (eglot--capabilities proc) capabilities) (setf (eglot--status proc) nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc))) (when success-fn (funcall success-fn proc)) (eglot--notify proc :initialized (eglot--obj :__dummy__ t))))))))) @@ -344,10 +347,7 @@ INTERACTIVE is t if called interactively." buffers in project %s." proc managed-major-mode - short-name) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))))))))) + short-name))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -360,11 +360,7 @@ INTERACTIVE is t if called interactively." (eglot--major-mode process) (eglot--short-name process) (eglot--contact process) - (lambda (proc) - (eglot--message "Reconnected!") - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc)))))) + (lambda (_proc) (eglot--message "Reconnected!")))) (defvar eglot--inhibit-auto-reconnect nil "If non-nil, don't autoreconnect on unexpected quit.") @@ -397,9 +393,11 @@ INTERACTIVE is t if called interactively." (process-exit-status process))) ((null eglot--inhibit-auto-reconnect) (eglot--warn - "(sentinel) Reconnecting after process unexpectedly changed to %s." + "(sentinel) Reconnecting after process unexpectedly changed to `%s'." change) - (eglot-reconnect process) + (condition-case-unless-debug err + (eglot-reconnect process) + (error (eglot--warn "Auto-reconnect failed: %s " err) )) (setq eglot--inhibit-auto-reconnect (run-with-timer 3 nil @@ -493,13 +491,22 @@ INTERACTIVE is t if called interactively." (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc type message &optional id error) +(defun eglot--log-event (proc message type) "Log an eglot-related event. -PROC is the current process. TYPE is an identifier. MESSAGE is -a JSON-like plist or anything else. ID is a continuation -identifier. ERROR is non-nil if this is a JSON-RPC error." +PROC is the current process. MESSAGE is a JSON-like plist. TYPE +is a symbol saying if this is a client or server originated." (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) + (let* ((inhibit-read-only t) + (id (plist-get message :id)) + (error (plist-get message :error)) + (method (plist-get message :method)) + (subtype (cond ((and method id) 'request) + (method 'notification) + (id 'reply) + ;; pyls keeps on sending these + (t 'unexpected-thingy))) + (type + (format "%s-%s" type subtype))) (goto-char (point-max)) (let ((msg (format "%s%s%s:\n%s\n" type @@ -518,15 +525,7 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (continuations (and id (not method) (gethash id (eglot--pending-continuations proc))))) - (eglot--log-event proc - (cond ((and method id) 'server-request) - (method 'server-notification) - (continuations 'server-reply) - ;; pyls keeps on sending these - (t 'unexpected-server-thingy)) - message - id - err) + (eglot--log-event proc message 'server) (when err (setf (eglot--status proc) '("error" t))) (cond (method ;; a server notification or a server request @@ -557,19 +556,13 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." (defvar eglot--expect-carriage-return nil) -(defun eglot--process-send (id proc message &optional reply) +(defun eglot--process-send (proc message) "Send MESSAGE to PROC (ID is optional)." - (let* ((json (json-encode message)) - (to-send (format "Content-Length: %d\r\n\r\n%s" - (string-bytes json) - json))) - (process-send-string proc to-send) - (eglot--log-event proc (if id - (if reply - 'client-reply - 'client-request) - 'client-notification) - message id (plist-get message :error)))) + (let ((json (json-encode message))) + (process-send-string proc (format "Content-Length: %d\r\n\r\n%s" + (string-bytes json) + json)) + (eglot--log-event proc message 'client))) (defvar eglot--next-request-id 0) @@ -614,8 +607,7 @@ identifier. ERROR is non-nil if this is a JSON-RPC error." "(request) Request id=%s replied to with result=%s: %s" id result-body))))) (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) - (eglot--process-send id - process + (eglot--process-send process (eglot--obj :jsonrpc "2.0" :id id :method method @@ -678,21 +670,18 @@ Meaning only return locally if successful, otherwise exit non-locally." (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" - (eglot--process-send nil - process + (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :id nil :method method :params params))) (cl-defun eglot--reply (process id &key result error) "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send id process + (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :id nil + :id id :result result - :error error) - t)) + :error error))) ;;; Helpers @@ -1057,28 +1046,29 @@ running. INTERACTIVE is t if called interactively." (cl-defun eglot--server-client/registerCapability (proc &key id registrations) "Handle notification client/registerCapability" - (catch 'done - (mapc - (lambda (reg) - (apply - (cl-function - (lambda (&key id method registerOptions) - (pcase-let* - ((handler-sym (intern (concat "eglot--register-" - method))) - (`(,ok ,message) - (and (functionp handler-sym) - (apply handler-sym proc :id id registerOptions)))) - (unless ok - (throw - 'done - (eglot--reply proc id - :error (eglot--obj - :code -32601 - :message (or message "sorry :-(")))))))) - reg)) - registrations) - (eglot--reply proc id :result (eglot--obj :message "OK")))) + (let ((jsonrpc-id id)) + (catch 'done + (mapc + (lambda (reg) + (apply + (cl-function + (lambda (&key id method registerOptions) + (pcase-let* + ((handler-sym (intern (concat "eglot--register-" + method))) + (`(,ok ,message) + (and (functionp handler-sym) + (apply handler-sym proc :id id registerOptions)))) + (unless ok + (throw + 'done + (eglot--reply proc jsonrpc-id + :error (eglot--obj + :code -32601 + :message (or message "sorry :-(")))))))) + reg)) + registrations) + (eglot--reply proc id :result (eglot--obj :message "OK"))))) (defvar eglot--recent-before-changes nil "List of recent changes as collected by `eglot--before-change'.") @@ -1236,8 +1226,9 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (defvar eglot--xref-known-symbols nil) -(defun eglot--xref-reset-known-symbols () - "Reset `eglot--xref-reset-known-symbols'." +(defun eglot--xref-reset-known-symbols (&rest _dummy) + "Reset `eglot--xref-reset-known-symbols'. +DUMMY is ignored" (setq eglot--xref-known-symbols nil)) (advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) From 963f9f4bf90e6a08ffe569155d4b7620545bc60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 12:18:08 +0100 Subject: [PATCH 076/771] Etoomanylambdas * eglot.el (eglot--sync-request): Remove a lambda. --- lisp/progmodes/eglot.el | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d6ecdfed188..321307df29a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -663,9 +663,8 @@ Meaning only return locally if successful, otherwise exit non-locally." (lambda (&key code message &allow-other-keys) (eglot--error "Oops: %s: %s" code message))) :timeout-fn (lambda () - (lambda () - (eglot--error - "Tired of waiting for reply to sync request"))) + (eglot--error + "Tired of waiting for reply to sync request")) :async-p nil)) (cl-defun eglot--notify (process method params) From ab858c8ab1fa11aedac1499a3a3f47a066df8a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 13:42:56 +0100 Subject: [PATCH 077/771] Workaround two suspected emacs bugs * eglot.el (eglot--process-filter): Use a proper unique tag. Use unwind-protect. (eglot--sync-request): Rework. (eglot--server-client/registerCapability): Use a proper done tag. --- lisp/progmodes/eglot.el | 139 +++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 65 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 321307df29a..1d72b4c5244 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -415,7 +415,8 @@ INTERACTIVE is t if called interactively." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) (let ((inhibit-read-only t) - (expected-bytes (eglot--expected-bytes proc))) + (expected-bytes (eglot--expected-bytes proc)) + (done (make-symbol "eglot--process-filter-done-tag"))) ;; Insert the text, advancing the process marker. ;; (save-excursion @@ -424,51 +425,52 @@ INTERACTIVE is t if called interactively." (set-marker (process-mark proc) (point))) ;; Loop (more than one message might have arrived) ;; - (catch 'done - (while t - (cond ((not expected-bytes) - ;; Starting a new message - ;; - (setq expected-bytes - (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \ -*\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t) - (string-to-number (match-string 1)))) - (unless expected-bytes - (throw 'done :waiting-for-new-message))) - (t - ;; Attempt to complete a message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; Process content in another buffer, - ;; shielding buffer from tamper - ;; - (with-temp-buffer - (eglot--process-receive proc json-message)))) - (goto-char message-end) - (delete-region (point-min) (point)) - (setq expected-bytes nil)))) - (t - ;; Message is still incomplete + (unwind-protect + (catch done + (while t + (cond ((not expected-bytes) + ;; Starting a new message ;; - (throw 'done :waiting-for-more-bytes-in-this-message)))))))) - ;; Saved parsing state for next visit to this filter - ;; - (setf (eglot--expected-bytes proc) expected-bytes))))) + (setq expected-bytes + (and (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \ +*\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" + (+ (point) 100) + t) + (string-to-number (match-string 1)))) + (unless expected-bytes + (throw done :waiting-for-new-message))) + (t + ;; Attempt to complete a message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; Process content in another buffer, + ;; shielding buffer from tamper + ;; + (with-temp-buffer + (eglot--process-receive proc json-message)))) + (goto-char message-end) + (delete-region (point-min) (point)) + (setq expected-bytes nil)))) + (t + ;; Message is still incomplete + ;; + (throw done :waiting-for-more-bytes-in-this-message)))))))) + ;; Saved parsing state for next visit to this filter + ;; + (setf (eglot--expected-bytes proc) expected-bytes)))))) (defun eglot-events-buffer (process &optional interactive) "Display events buffer for current LSP connection PROCESS. @@ -654,18 +656,25 @@ is a symbol saying if this is a client or server originated." (defun eglot--sync-request (proc method params) "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally." - (eglot--request proc method params - :success-fn (lambda (&rest args) - (if (vectorp (car args)) - (car args) - args)) - :error-fn (cl-function - (lambda (&key code message &allow-other-keys) - (eglot--error "Oops: %s: %s" code message))) - :timeout-fn (lambda () - (eglot--error - "Tired of waiting for reply to sync request")) - :async-p nil)) + (let* ((timeout-error-sym (cl-gensym)) + (retval (eglot--request proc method params + :success-fn (lambda (&rest args) + (if (vectorp (car args)) + (car args) + args)) + :error-fn (cl-function + (lambda (&key code message &allow-other-keys) + (eglot--error "Oops: %s: %s" code message))) + :timeout-fn (lambda () timeout-error-sym) + :async-p nil))) + ;; FIXME: There's maybe an emacs bug here. Because timeout-fn runs + ;; in a timer, the better and obvious choice of throwing the erro + ;; in the lambda is not quitting the `accept-process-output' + ;; infinite loop up there. So use this contorted strategy with + ;; `cl-gensym'. + (if (eq retval timeout-error-sym) + (eglot--error "Tired of waiting for reply to sync request") + retval))) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" @@ -1045,8 +1054,9 @@ running. INTERACTIVE is t if called interactively." (cl-defun eglot--server-client/registerCapability (proc &key id registrations) "Handle notification client/registerCapability" - (let ((jsonrpc-id id)) - (catch 'done + (let ((jsonrpc-id id) + (done (make-symbol "done"))) + (catch done (mapc (lambda (reg) (apply @@ -1059,12 +1069,11 @@ running. INTERACTIVE is t if called interactively." (and (functionp handler-sym) (apply handler-sym proc :id id registerOptions)))) (unless ok - (throw - 'done - (eglot--reply proc jsonrpc-id - :error (eglot--obj - :code -32601 - :message (or message "sorry :-(")))))))) + (throw done + (eglot--reply proc jsonrpc-id + :error (eglot--obj + :code -32601 + :message (or message "sorry :-(")))))))) reg)) registrations) (eglot--reply proc id :result (eglot--obj :message "OK"))))) From c5232c581e0ed714edb2cf30bd59f23511bd2fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 13:45:10 +0100 Subject: [PATCH 078/771] Increase request timeout length to 10 seconds * eglot.el (eglot-request-timeout): New var. (eglot--request): Use it. --- lisp/progmodes/eglot.el | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1d72b4c5244..cd485196b2e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -48,8 +48,11 @@ (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) - "Face for package-name in EGLOT's mode line." - :group 'eglot) + "Face for package-name in EGLOT's mode line.") + +(defcustom eglot-request-timeout 10 + "How many seconds to way for a reply from the server." + :type :integer) ;;; Process management @@ -617,7 +620,7 @@ is a symbol saying if this is a client or server originated." (catch catch-tag (let ((timeout-timer (run-with-timer - 5 nil + eglot-request-timeout nil (if async-p (lambda () (remhash id (eglot--pending-continuations process)) From 083ed923a7517ca63d4f259f4bfee7b8986ea03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 15:04:02 +0100 Subject: [PATCH 079/771] Support javascript's javascript-typescript-langserver * README.md: Improve a bit * eglot.el (eglot--make-process): Take MANAGED-MAJOR-MODE arg (eglot-executables): Add basic javascript support. (eglot--connect): Pass mode to eglot--make-process (eglot--interactive): Check that guessed command is a listp. (eglot): Minor improvement to message. (eglot--current-buffer-TextDocumentItem): Guess language from mode symbol. --- lisp/progmodes/eglot.el | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd485196b2e..045588de997 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -42,8 +42,10 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-executables '((rust-mode . ("rls")) - (python-mode . ("pyls"))) +(defvar eglot-executables + '((rust-mode . ("rls")) + (python-mode . ("pyls")) + (js-mode . ("javascript-typescript-stdio"))) "Alist mapping major modes to server executables.") (defface eglot-mode-line @@ -145,23 +147,24 @@ list of a single string of the form :") (eglot--define-process-var eglot--buffer-open-count (make-hash-table) "Keeps track of didOpen/didClose notifs for each buffer.") -(defun eglot--make-process (name contact) +(defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is a name to give the inferior process or connection. +MANAGED-MAJOR-MODE is a symbol naming a major mode. CONTACT is as `eglot--contact'. Returns a process object." - (let* ((readable-name (format "EGLOT server (%s)" name)) + (let* ((readable-name (format "EGLOT server (%s/%s)" name managed-major-mode)) (buffer (get-buffer-create (format "*%s inferior*" readable-name))) - (singleton (and (null (cdr contact)) (car contact))) + singleton (proc - (if (and - singleton - (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" - singleton)) + (if (and (setq singleton (and (null (cdr contact)) (car contact))) + (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" + singleton)) (open-network-stream readable-name buffer (match-string 1 singleton) - (string-to-number (match-string 2 singleton))) + (string-to-number + (match-string 2 singleton))) (make-process :name readable-name :buffer buffer @@ -236,7 +239,7 @@ CONTACT is as `eglot--contact'. Returns a process object." short-name contact &optional success-fn) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. SUCCESS-FN with no args if all goes well." - (let* ((proc (eglot--make-process short-name contact)) + (let* ((proc (eglot--make-process short-name managed-major-mode contact)) (buffer (process-buffer proc))) (setf (eglot--contact proc) contact (eglot--project proc) project @@ -297,15 +300,16 @@ SUCCESS-FN with no args if all goes well." managed-major-mode (let ((prompt (cond (current-prefix-arg - "[eglot] Execute program (or connect to :) ") + "[eglot] Enter program to execute (or :): ") ((null guessed-command) (format "[eglot] Sorry, couldn't guess for `%s'!\n\ -Execute program (or connect to :) " +Enter program to execute (or :): " managed-major-mode))))) (if prompt (split-string-and-unquote (read-shell-command prompt - (combine-and-quote-strings guessed-command) + (if (listp guessed-command) + (combine-and-quote-strings guessed-command)) 'eglot-command-history)) guessed-command)) t))) @@ -347,7 +351,7 @@ INTERACTIVE is t if called interactively." command (lambda (proc) (eglot--message "Connected! Process `%s' now managing `%s' \ -buffers in project %s." +buffers in project `%s'." proc managed-major-mode short-name))))))) @@ -1101,9 +1105,10 @@ running. INTERACTIVE is t if called interactively." "Compute TextDocumentItem object for current buffer." (append (eglot--current-buffer-VersionedTextDocumentIdentifier) - (eglot--obj :languageId (cdr (assoc major-mode - '((rust-mode . rust) - (emacs-lisp-mode . emacs-lisp)))) + (eglot--obj :languageId + (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode)) + "unknown") :text (save-restriction (widen) From b9a3366a428e011ac42c97cdfe891c739ab1b11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 16:32:23 +0100 Subject: [PATCH 080/771] Solve another textdocument/didchange bug * eglot.el (eglot--signal-textDocument/didChange): Rework a bit. (eglot--after-change): Store the actual after-text in the eglot--recent-after-changes. --- lisp/progmodes/eglot.el | 99 +++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 43 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 045588de997..f6bf01073e4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1128,52 +1128,65 @@ were deleted/added)" "Hook onto `after-change-functions'. Records START, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (push (list start end pre-change-length) eglot--recent-after-changes)) + (push (list start end pre-change-length + (buffer-substring-no-properties start end)) + eglot--recent-after-changes)) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." - (when (and eglot--recent-before-changes - eglot--recent-after-changes) - (let* ((proc (eglot--current-process-or-lose)) - (sync-kind (plist-get (eglot--capabilities proc) :textDocumentSync))) - (save-restriction - (widen) - (unless (or (not sync-kind) - (eq sync-kind 0)) - (eglot--notify - proc - :textDocument/didChange - (eglot--obj - :textDocument - (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges - (if (or (eq sync-kind 1) - (/= (length eglot--recent-before-changes) - (length eglot--recent-after-changes))) - (vector - (eglot--obj - :text (buffer-substring-no-properties (point-min) (point-max)))) - (apply - #'vector - (mapcar - (pcase-lambda (`(,before-start-position - ,before-end-position - ,after-start - ,after-end - ,len)) - (eglot--obj - :range - (eglot--obj - :start before-start-position - :end before-end-position) - :rangeLength len - :text (buffer-substring-no-properties after-start after-end))) - (reverse - (cl-mapcar 'append - eglot--recent-before-changes - eglot--recent-after-changes))))))))))) - (setq eglot--recent-before-changes nil - eglot--recent-after-changes nil)) + (unwind-protect + (when (or eglot--recent-before-changes + eglot--recent-after-changes) + (let* ((proc (eglot--current-process-or-lose)) + (sync-kind (plist-get (eglot--capabilities proc) + :textDocumentSync)) + (emacs-messup + (/= (length eglot--recent-before-changes) + (length eglot--recent-after-changes))) + (full-sync-p (or (eq sync-kind 1) emacs-messup))) + (when emacs-messup + (unless (eq sync-kind 1) + (eglot--warn "Using full sync because before: %s and after: %s" + eglot--recent-before-changes + eglot--recent-after-changes))) + (save-restriction + (widen) + (unless (or (not sync-kind) + (eq sync-kind 0)) + (eglot--notify + proc + :textDocument/didChange + (eglot--obj + :textDocument + (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (if full-sync-p + (vector + (eglot--obj + :text (buffer-substring-no-properties (point-min) + (point-max)))) + (apply + #'vector + (mapcar + (pcase-lambda (`(,before-start-position + ,before-end-position + ,_after-start + ,_after-end + ,len + ,after-text)) + (eglot--obj + :range + (eglot--obj + :start before-start-position + :end before-end-position) + :rangeLength len + :text after-text)) + (reverse + (cl-mapcar 'append + eglot--recent-before-changes + eglot--recent-after-changes))))))))))) + (setq eglot--recent-before-changes nil + eglot--recent-after-changes nil))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." From 2b307d5a114e53934d2bdd473136304ec7bd4ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 17:23:27 +0100 Subject: [PATCH 081/771] Half-decent completion support * README.md: Update. * eglot.el (eglot--kind-names): New variable. (eglot--managed-mode): Handle completion-at-point-functions. (eglot-completion-at-point): New function. --- lisp/progmodes/eglot.el | 55 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f6bf01073e4..2ab47819f02 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -757,6 +757,13 @@ Meaning only return locally if successful, otherwise exit non-locally." "Convert URI to a file path." (url-filename (url-generic-parse-url (url-unhex-string uri)))) +(defconst eglot--kind-names + `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") + (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") + (9 . "Module") (10 . "Property") (11 . "Unit") (12 . "Value") + (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") + (17 . "File") (18 . "Reference"))) + ;;; Minor modes ;;; @@ -777,10 +784,11 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) - ;; (add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) + ;;(add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) + (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (flymake-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) @@ -791,7 +799,8 @@ Meaning only return locally if successful, otherwise exit non-locally." ;; (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) - (remove-hook 'xref-backend-functions 'eglot-xref-backend t)))) + (remove-hook 'xref-backend-functions 'eglot-xref-backend t) + (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -1353,6 +1362,48 @@ DUMMY is ignored" :workspace/symbol (eglot--obj :query pattern)))) +(defun eglot-completion-at-point () + "EGLOT's `completion-at-point' function." + (let ((bounds (bounds-of-thing-at-point 'sexp)) + (proc (eglot--current-process-or-lose))) + (when (plist-get (eglot--capabilities proc) + :completionProvider) + (list + (if bounds (car bounds) (point)) + (if bounds (cdr bounds) (point)) + (completion-table-dynamic + (lambda (_ignored) + (let* ((resp (eglot--sync-request + proc + :textDocument/completion + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position)))) + (items (if (vectorp resp) resp + (plist-get resp :items)))) + (eglot--mapply + (eglot--lambda (&key insertText label kind detail + documentation sortText) + (propertize insertText + :label label :kind kind :detail detail + :documentation documentation :sortText sortText)) + items)))) + :annotation-function + (lambda (what) + (let ((detail (get-text-property 0 :detail what)) + (kind (get-text-property 0 :kind what))) + (format "%s%s" + detail + (if kind + (format " (%s)" (cdr (assoc kind eglot--kind-names))) + "")))) + :display-sort-function + (lambda (items) + (sort items (lambda (a b) + (string-lessp + (get-text-property 0 :sortText a) + (get-text-property 0 :sortText b))))))))) + ;;; Dynamic registration ;;; From 711d3a1d33d693eca9a1e21157518e1198b0ca9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 17:27:01 +0100 Subject: [PATCH 082/771] Explain why didopen on after-revert-hook is a bad idea The reason is that the global find-file-hook is called again, and that already does the didOpen. Too many didOpen's would be bad. * eglot.el (eglot--managed-mode): Remove commented lines. --- lisp/progmodes/eglot.el | 2 -- 1 file changed, 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ab47819f02..068e2ca5c54 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -784,7 +784,6 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) - ;;(add-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) @@ -796,7 +795,6 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) - ;; (remove-hook 'after-revert-hook 'eglot--signal-textDocument/didOpen t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) From 82c04790bd0d428b5f80bc32652f060498b1139f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 18:43:30 +0100 Subject: [PATCH 083/771] Tweak the async request engine. * eglot.el (eglot--request): Return the continuation id. (eglot--lambda): Move up in the file. (eglot--sync-request): Use a catch-tag. --- lisp/progmodes/eglot.el | 44 ++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 068e2ca5c54..3bed8d9344f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -593,7 +593,9 @@ is a symbol saying if this is a client or server originated." method params &key success-fn error-fn timeout-fn (async-p t)) - "Make a request to PROCESS, expecting a reply." + "Make a request to PROCESS, expecting a reply. +Return the ID of this request, unless ASYNC-P is nil, in which +case never returns locally." (let* ((id (eglot--next-request-id)) (timeout-fn (or timeout-fn @@ -658,22 +660,34 @@ is a symbol saying if this is a client or server originated." (when (memq timeout-timer timer-list) (eglot--message "(request) Last-change cancelling timer for continuation %s" id) - (cancel-timer timeout-timer)))))))) + (cancel-timer timeout-timer)))))) + ;; Finally, return the id. + id)) + +(cl-defmacro eglot--lambda (cl-lambda-list &body body) + (declare (indent 1) (debug (sexp &rest form))) + `(cl-function + (lambda ,cl-lambda-list + ,@body))) (defun eglot--sync-request (proc method params) "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally." (let* ((timeout-error-sym (cl-gensym)) - (retval (eglot--request proc method params - :success-fn (lambda (&rest args) - (if (vectorp (car args)) - (car args) - args)) - :error-fn (cl-function - (lambda (&key code message &allow-other-keys) - (eglot--error "Oops: %s: %s" code message))) - :timeout-fn (lambda () timeout-error-sym) - :async-p nil))) + (catch-tag (make-symbol "eglot--sync-request-catch-tag")) + (retval + (catch catch-tag + (eglot--request proc method params + :success-fn (lambda (&rest args) + (throw catch-tag (if (vectorp (car args)) + (car args) + args))) + :error-fn (eglot--lambda + (&key code message &allow-other-keys) + (eglot--error "Oops: %s: %s" code message)) + :timeout-fn (lambda () + (throw catch-tag timeout-error-sym)) + :async-p nil)))) ;; FIXME: There's maybe an emacs bug here. Because timeout-fn runs ;; in a timer, the better and obvious choice of throwing the erro ;; in the lambda is not quitting the `accept-process-output' @@ -741,12 +755,6 @@ Meaning only return locally if successful, otherwise exit non-locally." "Apply FUN to every element of SEQ." (mapcar (lambda (e) (apply fun e)) seq)) -(cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1) (debug (sexp &rest form))) - `(cl-function - (lambda ,cl-lambda-list - ,@body))) - (defun eglot--path-to-uri (path) "Urify PATH." (url-hexify-string From 72712e5aed3716aae1e66930e8e899f12e85c5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 18:45:57 +0100 Subject: [PATCH 084/771] Half-baked textdocument/hover support * eglot.el (eglot--format-markup): New helper. (eglot--managed-mode): Handle eldoc-documentation-function. (eglot-eldoc-function): New function. * README.md: update --- lisp/progmodes/eglot.el | 42 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3bed8d9344f..2c97f2ff48e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -772,6 +772,23 @@ Meaning only return locally if successful, otherwise exit non-locally." (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") (17 . "File") (18 . "Reference"))) +(defun eglot--format-markup (markup) + "Format MARKUP according to LSP's spec." + (cond ((stringp markup) + (with-temp-buffer + (ignore-errors (funcall 'markdown-mode)) + (font-lock-ensure) + (insert markup) + (string-trim (buffer-string)))) + (t + (with-temp-buffer + (ignore-errors (funcall (intern (concat + (plist-get markup :language) + "-mode" )))) + (insert (plist-get markup :value)) + (font-lock-ensure) + (buffer-string))))) + ;;; Minor modes ;;; @@ -796,7 +813,10 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) - (flymake-mode 1)) + (add-function :before-until (local 'eldoc-documentation-function) + #'eglot-eldoc-function) + (flymake-mode 1) + (eldoc-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) @@ -806,7 +826,9 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) - (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t)))) + (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) + (remove-function (local 'eldoc-documentation-function) + #'eglot-eldoc-function)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -1410,6 +1432,22 @@ DUMMY is ignored" (get-text-property 0 :sortText a) (get-text-property 0 :sortText b))))))))) +(defun eglot-eldoc-function () + "EGLOT's `eldoc-documentation-function' function." + (eglot--request (eglot--current-process-or-lose) + :textDocument/hover + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position)) + :success-fn (eglot--lambda (&key contents _range) + (eldoc-message + (mapconcat #'eglot--format + (if (vectorp contents) + contents + (list contents)) + "\n")))) + nil) + ;;; Dynamic registration ;;; From a3fb899cb5bb6b88f2bf117578a273c81e4cb08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 18:46:28 +0100 Subject: [PATCH 085/771] Clean up client capabilities * eglot.el (eglot--client-capabilities): Clean up client capabilities. --- lisp/progmodes/eglot.el | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2c97f2ff48e..8e927258be7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -215,23 +215,10 @@ CONTACT is as `eglot--contact'. Returns a process object." :willSave t :willSaveWaitUntil :json-false :didSave t) - :completion nil - :hover nil - :signatureHelp nil - :references nil - :documentHighlight nil - :documentSymbol nil - :formatting nil - :rangeFormatting nil - :onTypeFormatting nil - :definition nil - :typeDefinition nil - :implementation nil - :codeAction nil - :codeLens nil - :documentLink nil - :colorProvider nil - :rename nil + :completion `(:dynamicRegistration :json-false) + :hover `(:dynamicRegistration :json-false) + :references `(:dynamicRegistration :json-false) + :definition `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) From 283cfbcd4d53e22ecacdce33bbfe7a2a3b457a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 22:06:49 +0100 Subject: [PATCH 086/771] Fix bug in hover support * eldoc.el (eglot-eldoc-function): Use eglot--format-markup. (subr-x): Require it. (eglot--format-markup): Pacify byte-compiler. --- lisp/progmodes/eglot.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8e927258be7..9d7b16d39fd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -34,6 +34,7 @@ (require 'warnings) (require 'flymake) (require 'xref) +(require 'subr-x) ;;; User tweakable stuff @@ -763,7 +764,7 @@ Meaning only return locally if successful, otherwise exit non-locally." "Format MARKUP according to LSP's spec." (cond ((stringp markup) (with-temp-buffer - (ignore-errors (funcall 'markdown-mode)) + (ignore-errors (funcall (intern "markdown-mode"))) ;escape bytecompiler (font-lock-ensure) (insert markup) (string-trim (buffer-string)))) @@ -1428,7 +1429,7 @@ DUMMY is ignored" :position (eglot--pos-to-lsp-position)) :success-fn (eglot--lambda (&key contents _range) (eldoc-message - (mapconcat #'eglot--format + (mapconcat #'eglot--format-markup (if (vectorp contents) contents (list contents)) From f1b7d1d15c8c5a99267ef398d58eb6b862d76ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 22:14:06 +0100 Subject: [PATCH 087/771] * eglot.el: reformat to shave off some lines. --- lisp/progmodes/eglot.el | 268 ++++++++++++++++------------------------ 1 file changed, 108 insertions(+), 160 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9d7b16d39fd..ba3b5bee71e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -43,10 +43,9 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-executables - '((rust-mode . ("rls")) - (python-mode . ("pyls")) - (js-mode . ("javascript-typescript-stdio"))) +(defvar eglot-executables '((rust-mode . ("rls")) + (python-mode . ("pyls")) + (js-mode . ("javascript-typescript-stdio"))) "Alist mapping major modes to server executables.") (defface eglot-mode-line @@ -80,8 +79,7 @@ "Return the current EGLOT process or error." (or (eglot--current-process) (eglot--error "No current EGLOT process%s" - (if (project-current) "" - " (Also no current project)")))) + (if (project-current) "" " (Also no current project)")))) (defmacro eglot--define-process-var (var-sym initval &optional doc mode-line-update-p) @@ -166,13 +164,12 @@ CONTACT is as `eglot--contact'. Returns a process object." (match-string 1 singleton) (string-to-number (match-string 2 singleton))) - (make-process - :name readable-name - :buffer buffer - :command contact - :connection-type 'pipe - :stderr (get-buffer-create (format "*%s stderr*" - name)))))) + (make-process :name readable-name + :buffer buffer + :command contact + :connection-type 'pipe + :stderr (get-buffer-create (format "*%s stderr*" + name)))))) (set-process-filter proc #'eglot--process-filter) (set-process-sentinel proc #'eglot--process-sentinel) proc)) @@ -186,9 +183,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (defun eglot--project-short-name (project) "Give PROJECT a short name." - (file-name-base - (directory-file-name - (car (project-roots project))))) + (file-name-base (directory-file-name (car (project-roots project))))) (defun eglot--all-major-modes () "Return all know major modes." @@ -202,14 +197,7 @@ CONTACT is as `eglot--contact'. Returns a process object." "What the EGLOT LSP client supports." (eglot--obj :workspace (eglot--obj - :applyEdit nil - :workspaceEdit nil - :didChangeConfiguration nil - :didChangeWatchedFiles nil - :symbol nil - :executeCommand nil - :workspaceFolders nil - :configuration nil) + :symbol `(:dynamicRegistration :json-false)) :textDocument (eglot--obj :synchronization (eglot--obj :dynamicRegistration :json-false @@ -277,8 +265,7 @@ SUCCESS-FN with no args if all goes well." (intern (completing-read "[eglot] Start a server to manage buffers of what major mode? " - (mapcar #'symbol-name - (eglot--all-major-modes)) nil t + (mapcar #'symbol-name (eglot--all-major-modes)) nil t (symbol-name major-mode) nil (symbol-name major-mode) nil))) (t major-mode))) @@ -332,17 +319,16 @@ INTERACTIVE is t if called interactively." (eglot-reconnect current-process interactive) (when (process-live-p current-process) (eglot-shutdown current-process 'sync)) - (eglot--connect - project - managed-major-mode - short-name - command - (lambda (proc) - (eglot--message "Connected! Process `%s' now managing `%s' \ -buffers in project `%s'." - proc - managed-major-mode - short-name))))))) + (eglot--connect project + managed-major-mode + short-name + command + (lambda (proc) + (eglot--message "Connected! Process `%s' now \ +managing `%s' buffers in project `%s'." + proc + managed-major-mode + short-name))))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -350,12 +336,11 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) (when (process-live-p process) (eglot-shutdown process 'sync interactive)) - (eglot--connect - (eglot--project process) - (eglot--major-mode process) - (eglot--short-name process) - (eglot--contact process) - (lambda (_proc) (eglot--message "Reconnected!")))) + (eglot--connect (eglot--project process) + (eglot--major-mode process) + (eglot--short-name process) + (eglot--contact process) + (lambda (_proc) (eglot--message "Reconnected!")))) (defvar eglot--inhibit-auto-reconnect nil "If non-nil, don't autoreconnect on unexpected quit.") @@ -484,8 +469,7 @@ INTERACTIVE is t if called interactively." eglot--special-buffer-process process) (eglot-mode)) buffer)))) - (when interactive - (display-buffer buffer)) + (when interactive (display-buffer buffer)) buffer)) (defun eglot--log-event (proc message type) @@ -585,26 +569,23 @@ is a symbol saying if this is a client or server originated." Return the ID of this request, unless ASYNC-P is nil, in which case never returns locally." (let* ((id (eglot--next-request-id)) - (timeout-fn - (or timeout-fn - (lambda () - (eglot--warn - "(request) Tired of waiting for reply to %s" id)))) - (error-fn - (or error-fn - (cl-function - (lambda (&key code message &allow-other-keys) - (setf (eglot--status process) '("error" t)) - (eglot--warn - "(request) Request id=%s errored with code=%s: %s" - id code message))))) - (success-fn - (or success-fn - (cl-function - (lambda (&rest result-body) - (eglot--debug - "(request) Request id=%s replied to with result=%s: %s" - id result-body))))) + (timeout-fn (or timeout-fn + (lambda () + (eglot--warn + "(request) Tired of waiting for reply to %s" id)))) + (error-fn (or error-fn + (cl-function + (lambda (&key code message &allow-other-keys) + (setf (eglot--status process) '("error" t)) + (eglot--warn + "(request) Request id=%s errored with code=%s: %s" + id code message))))) + (success-fn (or success-fn + (cl-function + (lambda (&rest result-body) + (eglot--debug + "(request) Request id=%s replied to with result=%s" + id result-body))))) (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) (eglot--process-send process (eglot--obj :jsonrpc "2.0" @@ -654,9 +635,7 @@ case never returns locally." (cl-defmacro eglot--lambda (cl-lambda-list &body body) (declare (indent 1) (debug (sexp &rest form))) - `(cl-function - (lambda ,cl-lambda-list - ,@body))) + `(cl-function (lambda ,cl-lambda-list ,@body))) (defun eglot--sync-request (proc method params) "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. @@ -687,18 +666,16 @@ Meaning only return locally if successful, otherwise exit non-locally." (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" - (eglot--process-send process - (eglot--obj :jsonrpc "2.0" - :method method - :params params))) + (eglot--process-send process (eglot--obj :jsonrpc "2.0" + :method method + :params params))) (cl-defun eglot--reply (process id &key result error) "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send process - (eglot--obj :jsonrpc "2.0" - :id id - :result result - :error error))) + (eglot--process-send process (eglot--obj :jsonrpc "2.0" + :id id + :result result + :error error))) ;;; Helpers @@ -745,9 +722,8 @@ Meaning only return locally if successful, otherwise exit non-locally." (defun eglot--path-to-uri (path) "Urify PATH." - (url-hexify-string - (concat "file://" (file-truename path)) - url-path-allowed-chars)) + (url-hexify-string (concat "file://" (file-truename path)) + url-path-allowed-chars)) (defun eglot--uri-to-path (uri) "Convert URI to a file path." @@ -782,13 +758,9 @@ Meaning only return locally if successful, otherwise exit non-locally." ;;; (defvar eglot-mode-map (make-sparse-keymap)) -(defvar eglot--managed-mode-map (make-sparse-keymap)) - (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." - nil - nil - eglot-mode-map + nil nil eglot-mode-map (cond (eglot--managed-mode (eglot-mode 1) @@ -824,10 +796,9 @@ Meaning only return locally if successful, otherwise exit non-locally." (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer is managed by PROC." - (and buffer-file-name - (let ((cur (eglot--current-process))) - (or (and (null proc) cur) - (and proc (eq proc cur)))))) + (and buffer-file-name (let ((cur (eglot--current-process))) + (or (and (null proc) cur) + (and proc (eq proc cur)))))) (defun eglot--maybe-activate-editing-mode (&optional proc) "Maybe activate mode function `eglot--managed-mode'. @@ -846,11 +817,9 @@ that case, also signal textDocument/didOpen." ;;; (defvar eglot-menu) -(easy-menu-define eglot-menu eglot-mode-map "EGLOT" - `("EGLOT" )) +(easy-menu-define eglot-menu eglot-mode-map "EGLOT" `("EGLOT" )) -(defvar eglot--mode-line-format - `(:eval (eglot--mode-line-format))) +(defvar eglot--mode-line-format `(:eval (eglot--mode-line-format))) (put 'eglot--mode-line-format 'risky-local-variable t) @@ -949,9 +918,7 @@ that case, also signal textDocument/didOpen." (eglot--mode-line-call 'eglot-forget-pending-continuations)) map))))))))) -(add-to-list 'mode-line-misc-info - `(eglot-mode - (" [" eglot--mode-line-format "] "))) +(add-to-list 'mode-line-misc-info `(eglot-mode (" [" eglot--mode-line-format "] "))) ;;; Protocol implementation (Requests, notifications, etc) @@ -971,9 +938,7 @@ running. INTERACTIVE is t if called interactively." (setf (eglot--moribund process) t) (delete-process process)))) (eglot--request - process - :shutdown - nil + process :shutdown nil :success-fn (lambda (&rest _anything) (when interactive (eglot--message "Now asking %s politely to exit" process)) @@ -989,8 +954,7 @@ running. INTERACTIVE is t if called interactively." :async-p (not sync) :timeout-fn brutal))) -(cl-defun eglot--server-window/showMessage - (_process &key type message) +(cl-defun eglot--server-window/showMessage (_process &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) @@ -1018,15 +982,13 @@ running. INTERACTIVE is t if called interactively." :error (eglot--obj :code -32800 :message "User cancelled")))))) -(cl-defun eglot--server-window/logMessage - (_process &key type message) +(cl-defun eglot--server-window/logMessage (_process &key type message) "Handle notification window/logMessage" (eglot--log (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) -(cl-defun eglot--server-telemetry/event - (_process &rest any) +(cl-defun eglot--server-telemetry/event (_process &rest any) "Handle notification telemetry/event" (eglot--log "Server telemetry: %s" any)) @@ -1045,16 +1007,14 @@ running. INTERACTIVE is t if called interactively." (cond (buffer (with-current-buffer buffer - (cl-flet ((pos-at - (pos-plist) - (save-excursion - (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) - (forward-char - (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) - (point)))) + (cl-flet ((pos-at (pos-plist) + (save-excursion (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char + (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position)))) + (point)))) (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range severity _group _code source message) @@ -1066,12 +1026,9 @@ running. INTERACTIVE is t if called interactively." (flymake-make-diagnostic (current-buffer) begin-pos end-pos - (cond ((<= severity 1) - :error) - ((= severity 2) - :warning) - (t - :note)) + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) (concat source ": " message))))) into diags finally @@ -1199,17 +1156,13 @@ Records START, END and PRE-CHANGE-LENGTH locally." ,_after-end ,len ,after-text)) - (eglot--obj - :range - (eglot--obj - :start before-start-position - :end before-end-position) - :rangeLength len - :text after-text)) - (reverse - (cl-mapcar 'append - eglot--recent-before-changes - eglot--recent-after-changes))))))))))) + (eglot--obj :range (eglot--obj :start before-start-position + :end before-end-position) + :rangeLength len + :text after-text)) + (reverse (cl-mapcar 'append + eglot--recent-before-changes + eglot--recent-after-changes))))))))))) (setq eglot--recent-before-changes nil eglot--recent-after-changes nil))) @@ -1290,12 +1243,11 @@ DUMMY is ignored" (defun eglot--xref-make (name uri position) "Like `xref-make' but with LSP's NAME, URI and POSITION." - (xref-make name - (xref-make-file-location - (eglot--uri-to-path uri) - ;; F!@(#*&#$)CKING OFF-BY-ONE again - (1+ (plist-get position :line)) - (plist-get position :character)))) + (xref-make name (xref-make-file-location + (eglot--uri-to-path uri) + ;; F!@(#*&#$)CKING OFF-BY-ONE again + (1+ (plist-get position :line)) + (plist-get position :character)))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (let ((proc (eglot--current-process-or-lose)) @@ -1313,11 +1265,10 @@ DUMMY is ignored" :textDocument text-id :kind kind :containerName containerName)) - (eglot--sync-request - proc - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) + (eglot--sync-request proc + :textDocument/documentSymbol + (eglot--obj + :textDocument text-id)))) (all-completions string eglot--xref-known-symbols))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) @@ -1385,8 +1336,8 @@ DUMMY is ignored" (when (plist-get (eglot--capabilities proc) :completionProvider) (list - (if bounds (car bounds) (point)) - (if bounds (cdr bounds) (point)) + (or (car bounds) (point)) + (or (cdr bounds) (point)) (completion-table-dynamic (lambda (_ignored) (let* ((resp (eglot--sync-request @@ -1395,8 +1346,7 @@ DUMMY is ignored" (eglot--obj :textDocument (eglot--current-buffer-TextDocumentIdentifier) :position (eglot--pos-to-lsp-position)))) - (items (if (vectorp resp) resp - (plist-get resp :items)))) + (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail documentation sortText) @@ -1405,20 +1355,18 @@ DUMMY is ignored" :documentation documentation :sortText sortText)) items)))) :annotation-function - (lambda (what) - (let ((detail (get-text-property 0 :detail what)) - (kind (get-text-property 0 :kind what))) - (format "%s%s" - detail - (if kind - (format " (%s)" (cdr (assoc kind eglot--kind-names))) - "")))) + (lambda (what) (let ((detail (get-text-property 0 :detail what)) + (kind (get-text-property 0 :kind what))) + (format "%s%s" + detail + (if kind + (format " (%s)" (cdr (assoc kind eglot--kind-names))) + "")))) :display-sort-function - (lambda (items) - (sort items (lambda (a b) - (string-lessp - (get-text-property 0 :sortText a) - (get-text-property 0 :sortText b))))))))) + (lambda (items) (sort items (lambda (a b) + (string-lessp + (get-text-property 0 :sortText a) + (get-text-property 0 :sortText b))))))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." From 44cdd8062bd1d40caa76e91964a49e7acf3ae56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 22:15:21 +0100 Subject: [PATCH 088/771] Get rid of eglot--special-buffer-process Hasn't really proved useful yet. * eglot.el (eglot--special-buffer-process): Delete. (eglot--current-process): Simplify. (eglot--events-buffer): Simplify. --- lisp/progmodes/eglot.el | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ba3b5bee71e..1318feee511 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -61,19 +61,11 @@ (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defvar-local eglot--special-buffer-process nil - "Current buffer's eglot process.") - (defun eglot--current-process () "The current logical EGLOT process." - (or eglot--special-buffer-process - (let* ((cur (project-current)) - (processes - (and cur - (gethash cur eglot--processes-by-project)))) - (cl-find major-mode - processes - :key #'eglot--major-mode)))) + (let* ((cur (project-current)) + (processes (and cur (gethash cur eglot--processes-by-project)))) + (cl-find major-mode processes :key #'eglot--major-mode))) (defun eglot--current-process-or-lose () "Return the current EGLOT process or error." @@ -465,9 +457,7 @@ INTERACTIVE is t if called interactively." (with-current-buffer buffer (buffer-disable-undo) (read-only-mode t) - (setf (eglot--events-buffer process) buffer - eglot--special-buffer-process process) - (eglot-mode)) + (setf (eglot--events-buffer process) buffer)) buffer)))) (when interactive (display-buffer buffer)) buffer)) From ea51ade3a22979c5edca80042578daaaf78ff1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 22:16:28 +0100 Subject: [PATCH 089/771] Get rid of eglot--buffer-open-count Hasn't really proved useful yet. * eglot.el (eglot--buffer-open-count): Remove. (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didClose): Simplify. --- lisp/progmodes/eglot.el | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1318feee511..a600f4787bb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -135,9 +135,6 @@ A list (WHAT SERIOUS-P)." t) Either a list of strings (a shell command and arguments), or a list of a single string of the form :") -(eglot--define-process-var eglot--buffer-open-count (make-hash-table) - "Keeps track of didOpen/didClose notifs for each buffer.") - (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is a name to give the inferior process or connection. @@ -1158,33 +1155,17 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." - (let* ((proc (eglot--current-process-or-lose)) - (count (1+ (or (gethash (current-buffer) - (eglot--buffer-open-count proc)) - 0)))) - (when (> count 1) - (eglot--error "Too many textDocument/didOpen notifs for %s" (current-buffer))) - (setf (gethash (current-buffer) (eglot--buffer-open-count proc)) - count) - (eglot--notify proc - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem))))) + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didOpen + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (let* ((proc (eglot--current-process-or-lose)) - (count (1- (or (gethash (current-buffer) - (eglot--buffer-open-count proc)) - 0)))) - (when (< count 0) - (eglot--error "Too many textDocument/didClose notifs for %s" (current-buffer))) - (setf (gethash (current-buffer) (eglot--buffer-open-count proc)) - count) - (eglot--notify proc - :textDocument/didClose - (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentIdentifier))))) + (eglot--notify (eglot--current-process-or-lose) + :textDocument/didClose + (eglot--obj :textDocument + (eglot--current-buffer-TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." From 3299a6a4b6d7f7c907fbd6a22f249bbdd3174810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 22:40:53 +0100 Subject: [PATCH 090/771] Simplify mode-line code with a helper. * eglot.el (eglot--mdoe-line-props): New helper. (eglot--mode-line-format): Use it. --- lisp/progmodes/eglot.el | 118 +++++++++++++--------------------------- 1 file changed, 38 insertions(+), 80 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a600f4787bb..02439dfa040 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -817,93 +817,51 @@ that case, also signal textDocument/didOpen." (with-selected-window (posn-window (event-start event)) (call-interactively what)))) +(defun eglot--mode-line-props (thing face defs &optional prepend) + "Helper for function `eglot--mode-line-format'. +Uses THING, FACE, DEFS and PREPEND." + (cl-loop with map = (make-sparse-keymap) + for (elem . rest) on defs + for (key def help) = elem + do (define-key map `[mode-line ,key] (eglot--mode-line-call def)) + concat (format "%s: %s" key help) into blurb + when rest concat "\n" into blurb + finally (return `(:propertize ,thing + face ,face + keymap ,map help-echo ,(concat prepend blurb) + mouse-face mode-line-highlight)))) + (defun eglot--mode-line-format () - "Compose the mode-line format spec." + "Compose the EGLOT's mode-line." (pcase-let* ((proc (eglot--current-process)) - (name (and proc - (process-live-p proc) - (eglot--short-name proc))) - (pending (and proc - (hash-table-count - (eglot--pending-continuations proc)))) - (`(,_id ,doing ,done-p) - (and proc - (eglot--spinner proc))) - (`(,status ,serious-p) - (and proc - (eglot--status proc)))) + (name (and (process-live-p proc) (eglot--short-name proc))) + (pending (and proc (hash-table-count + (eglot--pending-continuations proc)))) + (`(,_id ,doing ,done-p) (and proc (eglot--spinner proc))) + (`(,status ,serious-p) (and proc (eglot--status proc)))) (append - `((:propertize "eglot" - face eglot-mode-line - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line down-mouse-1] - (eglot--mode-line-call 'eglot-menu)) - map) - mouse-face mode-line-highlight - help-echo "mouse-1: pop-up EGLOT menu" - )) + `(,(eglot--mode-line-props "eglot" 'eglot-mode-line + '((down-mouse-1 eglot-menu "pop up EGLOT menu")))) (when name - `(":" - (:propertize - ,name - face eglot-mode-line - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - (eglot--mode-line-call 'eglot-events-buffer)) - (define-key map [mode-line mouse-2] - (eglot--mode-line-call 'eglot-shutdown)) - (define-key map [mode-line mouse-3] - (eglot--mode-line-call 'eglot-reconnect)) - map) - mouse-face mode-line-highlight - help-echo ,(concat "mouse-1: go to events buffer\n" - "mouse-2: quit server\n" - "mouse-3: reconnect to server")) + `(":" ,(eglot--mode-line-props + name 'eglot-mode-line + '((mouse-1 eglot-events-buffer "go to events buffer") + (mouse-2 eglot-shutdown "quit server") + (mouse-3 eglot-reconnect "reconnect to server"))) ,@(when serious-p - `("/" - (:propertize - ,status - help-echo ,(concat "mouse-1: go to events buffer\n" - "mouse-3: clear this status") - mouse-face mode-line-highlight - face compilation-mode-line-fail - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - (eglot--mode-line-call 'eglot-events-buffer)) - (define-key map [mode-line mouse-3] - (eglot--mode-line-call 'eglot-clear-status)) - map)))) + `("/" ,(eglot--mode-line-props + status 'compilation-mode-line-fail + '((mouse-1 eglot-events-buffer "go to events buffer") + (mouse-3 eglot-clear-status "clear this status"))))) ,@(when (and doing (not done-p)) - `("/" - (:propertize - ,doing - help-echo ,(concat "mouse-1: go to events buffer") - mouse-face mode-line-highlight - face compilation-mode-line-run - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - (eglot--mode-line-call 'eglot-events-buffer)) - map)))) + `("/" ,(eglot--mode-line-props + doing 'compilation-mode-line-run + '((mouse-1 eglot-events-buffer "go to events buffer"))))) ,@(when (cl-plusp pending) - `("/" - (:propertize - (format "%d" pending) - help-echo ,(format - "%s unanswered requests\n%s" - pending - (concat "mouse-1: go to events buffer" - "mouse-3: forget pending continuations")) - mouse-face mode-line-highlight - face ,(cond ((and pending (cl-plusp pending)) - 'warning) - (t - 'eglot-mode-line)) - keymap ,(let ((map (make-sparse-keymap))) - (define-key map [mode-line mouse-1] - (eglot--mode-line-call 'eglot-events-buffer)) - (define-key map [mode-line mouse-3] - (eglot--mode-line-call 'eglot-forget-pending-continuations)) - map))))))))) + `("/" ,(eglot--mode-line-props + (format "%d" pending) 'warning + '((mouse-1 eglot-events-buffer "go to events buffer") + (mouse-3 eglot-clear-status "clear this status")))))))))) (add-to-list 'mode-line-misc-info `(eglot-mode (" [" eglot--mode-line-format "] "))) From fe01515f05c60b97f312b3b6b72c5540eb518107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 22:56:20 +0100 Subject: [PATCH 091/771] Only request stuff that server says it's capable of * eglot.el (eglot--server-capable): New helper. (eglot-xref-backend) (xref-backend-identifier-completion-table) (xref-backend-references, xref-backend-apropos) (eglot-completion-at-point, eglot-eldoc-function): Use it. --- lisp/progmodes/eglot.el | 98 ++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 02439dfa040..7cc375667dc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -740,6 +740,10 @@ Meaning only return locally if successful, otherwise exit non-locally." (font-lock-ensure) (buffer-string))))) +(defun eglot--server-capable (feat) + "Determine if current server is capable of FEAT." + (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) + ;;; Minor modes ;;; @@ -1158,7 +1162,9 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;; make the server report new diagnostics. (eglot--signal-textDocument/didChange)) -(defun eglot-xref-backend () "EGLOT xref backend." 'eglot) +(defun eglot-xref-backend () + "EGLOT xref backend." + (when (eglot--server-capable :definitionProvider) 'eglot)) (defvar eglot--xref-known-symbols nil) @@ -1179,26 +1185,27 @@ DUMMY is ignored" (plist-get position :character)))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (let ((proc (eglot--current-process-or-lose)) - (text-id (eglot--current-buffer-TextDocumentIdentifier))) - (completion-table-with-cache - (lambda (string) - (setq eglot--xref-known-symbols - (eglot--mapply - (eglot--lambda (&key name kind location containerName) - (propertize name - :position (plist-get - (plist-get location :range) - :start) - :locations (list location) - :textDocument text-id - :kind kind - :containerName containerName)) - (eglot--sync-request proc - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) - (all-completions string eglot--xref-known-symbols))))) + (when (eglot--server-capable :documentSymbolProvider) + (let ((proc (eglot--current-process-or-lose)) + (text-id (eglot--current-buffer-TextDocumentIdentifier))) + (completion-table-with-cache + (lambda (string) + (setq eglot--xref-known-symbols + (eglot--mapply + (eglot--lambda (&key name kind location containerName) + (propertize name + :position (plist-get + (plist-get location :range) + :start) + :locations (list location) + :textDocument text-id + :kind kind + :containerName containerName)) + (eglot--sync-request proc + :textDocument/documentSymbol + (eglot--obj + :textDocument text-id)))) + (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) (let ((symatpt (symbol-at-point))) @@ -1226,6 +1233,7 @@ DUMMY is ignored" location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) + (unless (eglot--server-capable :referencesProvider) (cl-return nil)) (let* ((identifier (if (get-text-property 0 :position identifier) identifier (car (member identifier eglot--xref-known-symbols)))) @@ -1234,8 +1242,7 @@ DUMMY is ignored" (textDocument (and identifier (get-text-property 0 :textDocument identifier)))) (unless (and position textDocument) - (eglot--error "Sorry, can't discover where %s is in the workspace" - identifier)) + (eglot--error "Don't know where %s is in the workspace" identifier)) (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) @@ -1249,21 +1256,21 @@ DUMMY is ignored" :context (eglot--obj :includeDeclaration t)))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) - (eglot--mapply - (eglot--lambda (&key name location &allow-other-keys) - (let ((range (plist-get location :range)) - (uri (plist-get location :uri))) - (eglot--xref-make name uri (plist-get range :start)))) - (eglot--sync-request (eglot--current-process-or-lose) - :workspace/symbol - (eglot--obj :query pattern)))) + (when (eglot--server-capable :workspaceSymbolProvider) + (eglot--mapply + (eglot--lambda (&key name location &allow-other-keys) + (let ((range (plist-get location :range)) + (uri (plist-get location :uri))) + (eglot--xref-make name uri (plist-get range :start)))) + (eglot--sync-request (eglot--current-process-or-lose) + :workspace/symbol + (eglot--obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'sexp)) (proc (eglot--current-process-or-lose))) - (when (plist-get (eglot--capabilities proc) - :completionProvider) + (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) @@ -1299,18 +1306,19 @@ DUMMY is ignored" (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." - (eglot--request (eglot--current-process-or-lose) - :textDocument/hover - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position)) - :success-fn (eglot--lambda (&key contents _range) - (eldoc-message - (mapconcat #'eglot--format-markup - (if (vectorp contents) - contents - (list contents)) - "\n")))) + (when (eglot--server-capable :hoverProvider) + (eglot--request (eglot--current-process-or-lose) + :textDocument/hover + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position)) + :success-fn (eglot--lambda (&key contents _range) + (eldoc-message + (mapconcat #'eglot--format-markup + (if (vectorp contents) + contents + (list contents)) + "\n"))))) nil) From 6fd613042e32b14762e7dc2492e80603e51b90ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 May 2018 23:43:03 +0100 Subject: [PATCH 092/771] Half-decent imenu support via textdocument/documentsymbol * README.md: Update capability * eglot.el (eglot--lsp-position-to-point): New function. (eglot--managed-mode): Handle imenu-create-index-function. (eglot--server-textDocument/publishDiagnostics): Use eglot--lsp-position-to-point. (eglot-imenu): New function. (eglot--client-capabilities): Capable of documentSymbol. --- lisp/progmodes/eglot.el | 96 ++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7cc375667dc..d7ea32977f8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -193,10 +193,11 @@ CONTACT is as `eglot--contact'. Returns a process object." :willSave t :willSaveWaitUntil :json-false :didSave t) - :completion `(:dynamicRegistration :json-false) - :hover `(:dynamicRegistration :json-false) - :references `(:dynamicRegistration :json-false) - :definition `(:dynamicRegistration :json-false) + :completion `(:dynamicRegistration :json-false) + :hover `(:dynamicRegistration :json-false) + :references `(:dynamicRegistration :json-false) + :definition `(:dynamicRegistration :json-false) + :documentSymbol `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) @@ -703,6 +704,17 @@ Meaning only return locally if successful, otherwise exit non-locally." (- (goto-char (or pos (point))) (line-beginning-position))))) +(defun eglot--lsp-position-to-point (pos-plist) + "Convert LSP position POS-PLIST to Emacs point." + (save-excursion (goto-char (point-min)) + (forward-line (plist-get pos-plist :line)) + (forward-char + (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position)))) + (point))) + + (defun eglot--mapply (fun seq) "Apply FUN to every element of SEQ." (mapcar (lambda (e) (apply fun e)) seq)) @@ -766,6 +778,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) + (advice-add imenu-create-index-function :around #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) (t @@ -779,7 +792,8 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'xref-backend-functions 'eglot-xref-backend t) (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) - #'eglot-eldoc-function)))) + #'eglot-eldoc-function) + (advice-remove imenu-create-index-function #'eglot-imenu)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -956,36 +970,28 @@ running. INTERACTIVE is t if called interactively." (cond (buffer (with-current-buffer buffer - (cl-flet ((pos-at (pos-plist) - (save-excursion (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) - (forward-char - (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) - (point)))) - (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity _group - _code source message) - diag-spec - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (pos-at start)) - (end-pos (pos-at end))) - (flymake-make-diagnostic - (current-buffer) - begin-pos end-pos - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message))))) - into diags - finally - (if eglot--current-flymake-report-fn - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics - diags)))))) + (cl-loop + for diag-spec across diagnostics + collect (cl-destructuring-bind (&key range severity _group + _code source message) + diag-spec + (cl-destructuring-bind (&key start end) + range + (let* ((begin-pos (eglot--lsp-position-to-point start)) + (end-pos (eglot--lsp-position-to-point end))) + (flymake-make-diagnostic + (current-buffer) + begin-pos end-pos + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message))))) + into diags + finally (if eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn + diags) + (setq eglot--unreported-diagnostics + diags))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -1321,6 +1327,26 @@ DUMMY is ignored" "\n"))))) nil) +(defun eglot-imenu (oldfun) + "EGLOT's `imenu-create-index-function' overriding OLDFUN." + (if (eglot--server-capable :documentSymbolProvider) + (let ((entries + (eglot--mapply + (eglot--lambda (&key name kind location _containerName) + (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) + (eglot--lsp-position-to-point + (plist-get (plist-get location :range) :start)))) + (eglot--sync-request + (eglot--current-process-or-lose) + :textDocument/documentSymbol + (eglot--obj + :textDocument (eglot--current-buffer-TextDocumentIdentifier)))))) + (append + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries) + entries)) + (funcall oldfun))) + ;;; Dynamic registration ;;; From 378a8371d289696ddf614e8b37a76358b16b418d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 8 May 2018 01:46:30 +0100 Subject: [PATCH 093/771] Try to fix some textdocument/completion bugs * eglot.el (eglot-completion-at-point): Rework slightly. --- lisp/progmodes/eglot.el | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d7ea32977f8..2e409ef840f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1274,13 +1274,13 @@ DUMMY is ignored" (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." - (let ((bounds (bounds-of-thing-at-point 'sexp)) + (let ((bounds (bounds-of-thing-at-point 'symbol)) (proc (eglot--current-process-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (completion-table-dynamic + (completion-table-with-cache (lambda (_ignored) (let* ((resp (eglot--sync-request proc @@ -1291,19 +1291,17 @@ DUMMY is ignored" (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail - documentation sortText) - (propertize insertText - :label label :kind kind :detail detail + documentation sortText &allow-other-keys) + (propertize (or insertText label) + :kind-name (cdr (assoc kind eglot--kind-names)) + :detail detail :documentation documentation :sortText sortText)) items)))) :annotation-function (lambda (what) (let ((detail (get-text-property 0 :detail what)) - (kind (get-text-property 0 :kind what))) - (format "%s%s" - detail - (if kind - (format " (%s)" (cdr (assoc kind eglot--kind-names))) - "")))) + (kind-name (get-text-property 0 :kind what))) + (concat (if detail (format " %s" detail) "") + (if kind-name (format " (%s)" kind-name) "")))) :display-sort-function (lambda (items) (sort items (lambda (a b) (string-lessp From 2c093aeb840f528270b3b3969e9506dd1f9a1ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 8 May 2018 01:52:27 +0100 Subject: [PATCH 094/771] When killing server, always wait 3 seconds * eglot.el (eglot--request): Accept TIMEOUT param. --- lisp/progmodes/eglot.el | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e409ef840f..9ce7e261a31 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -552,10 +552,12 @@ is a symbol saying if this is a client or server originated." (cl-defun eglot--request (process method params - &key success-fn error-fn timeout-fn (async-p t)) + &key success-fn error-fn timeout-fn (async-p t) + (timeout eglot-request-timeout)) "Make a request to PROCESS, expecting a reply. Return the ID of this request, unless ASYNC-P is nil, in which -case never returns locally." +case never returns locally. Wait TIMEOUT seconds for a +response." (let* ((id (eglot--next-request-id)) (timeout-fn (or timeout-fn (lambda () @@ -583,7 +585,7 @@ case never returns locally." (catch catch-tag (let ((timeout-timer (run-with-timer - eglot-request-timeout nil + timeout nil (if async-p (lambda () (remhash id (eglot--pending-continuations process)) @@ -912,10 +914,12 @@ running. INTERACTIVE is t if called interactively." :success-fn brutal :async-p (not sync) :error-fn brutal - :timeout-fn brutal)) + :timeout-fn brutal + :timeout 3)) :error-fn brutal :async-p (not sync) - :timeout-fn brutal))) + :timeout-fn brutal + :timeout 3))) (cl-defun eglot--server-window/showMessage (_process &key type message) "Handle notification window/showMessage" From dbe81138d6711aa7f6f02dda8ca644a92bdaea47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 8 May 2018 02:05:03 +0100 Subject: [PATCH 095/771] Fix odd bugs * eglot.el (eglot--process-receive, eglot--request): Set status to actual error message. (eglot--managed-mode): Manage imenu-create-index-function correctly. (eglot--mode-line-format): Print error status. --- lisp/progmodes/eglot.el | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9ce7e261a31..807df984af5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -495,7 +495,7 @@ is a symbol saying if this is a client or server originated." (not method) (gethash id (eglot--pending-continuations proc))))) (eglot--log-event proc message 'server) - (when err (setf (eglot--status proc) '("error" t))) + (when err (setf (eglot--status proc) `(,err t))) (cond (method ;; a server notification or a server request (let* ((handler-sym (intern (concat "eglot--server-" @@ -566,7 +566,7 @@ response." (error-fn (or error-fn (cl-function (lambda (&key code message &allow-other-keys) - (setf (eglot--status process) '("error" t)) + (setf (eglot--status process) `(,message t)) (eglot--warn "(request) Request id=%s errored with code=%s: %s" id code message))))) @@ -780,7 +780,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (advice-add imenu-create-index-function :around #'eglot-imenu) + (add-function :around (local imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) (t @@ -795,7 +795,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (advice-remove imenu-create-index-function #'eglot-imenu)))) + (remove-function (local imenu-create-index-function) #'eglot-imenu)))) (define-minor-mode eglot-mode "Minor mode for all buffers managed by EGLOT in some way." nil @@ -870,9 +870,10 @@ Uses THING, FACE, DEFS and PREPEND." (mouse-3 eglot-reconnect "reconnect to server"))) ,@(when serious-p `("/" ,(eglot--mode-line-props - status 'compilation-mode-line-fail + "error" 'compilation-mode-line-fail '((mouse-1 eglot-events-buffer "go to events buffer") - (mouse-3 eglot-clear-status "clear this status"))))) + (mouse-3 eglot-clear-status "clear this status")) + (format "An error occured: %s\n" status)))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props doing 'compilation-mode-line-run @@ -881,7 +882,8 @@ Uses THING, FACE, DEFS and PREPEND." `("/" ,(eglot--mode-line-props (format "%d" pending) 'warning '((mouse-1 eglot-events-buffer "go to events buffer") - (mouse-3 eglot-clear-status "clear this status")))))))))) + (mouse-3 eglot-clear-status "clear this status")) + (format "%d pending requests\n" pending))))))))) (add-to-list 'mode-line-misc-info `(eglot-mode (" [" eglot--mode-line-format "] "))) From 3d3c12faf230e63e1468a5283cfad28abef07649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 8 May 2018 11:38:02 +0100 Subject: [PATCH 096/771] Reasonable textdocument/documenthighlight support * README.md: Update. * eglot.el (eglot--current-buffer-TextDocumentPositionParams): New helper. (xref-backend-identifier-completion-table): Refactor a bit. (xref-backend-identifier-at-point): Use when-let and eglot--current-buffer-TextDocumentPositionParams (xref-backend-definitions, xref-backend-references): Refactor a bit. (eglot-completion-at-point): Use eglot--current-buffer-TextDocumentPositionParams (eglot-eldoc-function): Rewrite to handle textDocument/documentHighlight. (eglot--highlights): New variable. (eglot--client-capabilities): Update with support for documentHighlight. --- lisp/progmodes/eglot.el | 117 +++++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 807df984af5..c5c99f2e78e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -193,11 +193,12 @@ CONTACT is as `eglot--contact'. Returns a process object." :willSave t :willSaveWaitUntil :json-false :didSave t) - :completion `(:dynamicRegistration :json-false) - :hover `(:dynamicRegistration :json-false) - :references `(:dynamicRegistration :json-false) - :definition `(:dynamicRegistration :json-false) - :documentSymbol `(:dynamicRegistration :json-false) + :completion `(:dynamicRegistration :json-false) + :hover `(:dynamicRegistration :json-false) + :references `(:dynamicRegistration :json-false) + :definition `(:dynamicRegistration :json-false) + :documentSymbol `(:dynamicRegistration :json-false) + :documentHighlight `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) @@ -1057,6 +1058,11 @@ running. INTERACTIVE is t if called interactively." (widen) (buffer-substring-no-properties (point-min) (point-max)))))) +(defun eglot--current-buffer-TextDocumentPositionParams () + "Compute TextDocumentPositionParams." + (eglot--obj :textDocument (eglot--current-buffer-TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position))) + (defun eglot--before-change (start end) "Hook onto `before-change-functions'. Records START and END, crucially convert them into @@ -1206,11 +1212,12 @@ DUMMY is ignored" (eglot--mapply (eglot--lambda (&key name kind location containerName) (propertize name - :position (plist-get - (plist-get location :range) - :start) + :textDocumentPositionParams + (eglot--obj :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) :locations (list location) - :textDocument text-id :kind kind :containerName containerName)) (eglot--sync-request proc @@ -1220,11 +1227,10 @@ DUMMY is ignored" (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) - (let ((symatpt (symbol-at-point))) - (when symatpt - (propertize (symbol-name symatpt) - :textDocument (eglot--current-buffer-TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position))))) + (when-let ((symatpt (symbol-at-point))) + (propertize (symbol-name symatpt) + :textDocumentPositionParams + (eglot--current-buffer-TextDocumentPositionParams)))) (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) (let* ((rich-identifier @@ -1234,11 +1240,8 @@ DUMMY is ignored" (get-text-property 0 :locations rich-identifier) (eglot--sync-request (eglot--current-process-or-lose) :textDocument/definition - (eglot--obj - :textDocument - (get-text-property 0 :textDocument identifier) - :position - (get-text-property 0 :position identifier)))))) + (get-text-property + 0 :textDocumentPositionParams identifier))))) (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) @@ -1246,26 +1249,21 @@ DUMMY is ignored" (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) (cl-return nil)) - (let* ((identifier (if (get-text-property 0 :position identifier) - identifier - (car (member identifier eglot--xref-known-symbols)))) - (position - (and identifier (get-text-property 0 :position identifier))) - (textDocument - (and identifier (get-text-property 0 :textDocument identifier)))) - (unless (and position textDocument) - (eglot--error "Don't know where %s is in the workspace" identifier)) + (let ((params + (or (get-text-property 0 :textDocumentPositionParams identifier) + (let ((rich (car (member identifier eglot--xref-known-symbols)))) + (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) + (unless params + (eglot--error "Don' know where %s is in the workspace!" identifier)) (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) (eglot--sync-request (eglot--current-process-or-lose) :textDocument/references - (eglot--obj - :textDocument - textDocument - :position - position - :context (eglot--obj :includeDeclaration t)))))) + (append + params + (eglot--obj :context + (eglot--obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) @@ -1291,9 +1289,7 @@ DUMMY is ignored" (let* ((resp (eglot--sync-request proc :textDocument/completion - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position)))) + (eglot--current-buffer-TextDocumentPositionParams))) (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail @@ -1314,21 +1310,42 @@ DUMMY is ignored" (get-text-property 0 :sortText a) (get-text-property 0 :sortText b))))))))) +(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") + (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." - (when (eglot--server-capable :hoverProvider) - (eglot--request (eglot--current-process-or-lose) - :textDocument/hover - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position)) - :success-fn (eglot--lambda (&key contents _range) - (eldoc-message - (mapconcat #'eglot--format-markup - (if (vectorp contents) - contents - (list contents)) - "\n"))))) + (let ((buffer (current-buffer)) + (proc (eglot--current-process-or-lose)) + (position-params (eglot--current-buffer-TextDocumentPositionParams))) + (when (eglot--server-capable :hoverProvider) + (eglot--request proc :textDocument/hover position-params + :success-fn (eglot--lambda (&key contents _range) + (eldoc-message + (mapconcat #'eglot--format-markup + (if (vectorp contents) + contents + (list contents)) + "\n"))))) + (when (eglot--server-capable :documentHighlightProvider) + (eglot--request + proc :textDocument/documentHighlight position-params + :success-fn (lambda (highlights) + (mapc #'delete-overlay eglot--highlights) + (setq eglot--highlights + (when (get-buffer-window buffer) + (with-current-buffer buffer + (eglot--mapply + (eglot--lambda (&key range kind) + (cl-destructuring-bind (&key start end) range + (let ((ov (make-overlay + (eglot--lsp-position-to-point start) + (eglot--lsp-position-to-point end) + buffer))) + (overlay-put ov 'face 'highlight) + (overlay-put ov 'evaporate t) + (overlay-put ov :kind kind) + ov))) + highlights)))))))) nil) (defun eglot-imenu (oldfun) From ca678a54c8f2634c6c68f26f25837777b1211e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 8 May 2018 13:37:35 +0100 Subject: [PATCH 097/771] Support textdocument/rename * README.md: Mention rename support. * eglot.el (eglot--uri-to-path): Handle uri hidden in keywords. (eglot--apply-text-edits): New helper. (eglot-rename): New interactive command. (eglot--client-capabilities): Add rename capability. --- lisp/progmodes/eglot.el | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c5c99f2e78e..9ba87ef5bbc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -199,6 +199,7 @@ CONTACT is as `eglot--contact'. Returns a process object." :definition `(:dynamicRegistration :json-false) :documentSymbol `(:dynamicRegistration :json-false) :documentHighlight `(:dynamicRegistration :json-false) + :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) @@ -729,6 +730,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (defun eglot--uri-to-path (uri) "Convert URI to a file path." + (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) (url-filename (url-generic-parse-url (url-unhex-string uri)))) (defconst eglot--kind-names @@ -1368,6 +1370,62 @@ DUMMY is ignored" entries)) (funcall oldfun))) +(defun eglot--apply-text-edits (uri edits proc &optional version) + "Apply the EDITS for buffer of URI and return it." + (let* ((path (eglot--uri-to-path uri)) + (buffer (and path + (find-file-noselect path)))) + (unless buffer + (eglot--error "Can't find `%s' to perform server edits")) + (with-current-buffer buffer + (unless (eq proc (eglot--current-process)) + (eglot--error "Buffer `%s' for `%s' isn't managed by %s" + (current-buffer) uri proc)) + (unless (or (not version) + (equal version eglot--versioned-identifier)) + (eglot--error "Edits on `%s' require version %d, you have %d" + uri version eglot--versioned-identifier)) + (eglot--mapply + (eglot--lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (let ((start (eglot--lsp-position-to-point (plist-get range :start)))) + (goto-char start) + (delete-region start + (eglot--lsp-position-to-point (plist-get range :end))) + (insert newText))))) + edits) + (eglot--message "%s: %s edits" (current-buffer) (length edits))) + buffer)) + +(defun eglot-rename (newname) + "Rename the current symbol to NEWNAME." + (interactive + (list + (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point))))) + (unless (eglot--server-capable :renameProvider) + (eglot--error "Server can't rename!")) + (let* ((proc (eglot--current-process-or-lose)) + (workspace-edit + (eglot--sync-request proc + :textDocument/rename + (append + (eglot--current-buffer-TextDocumentPositionParams) + (eglot--obj :newName newname)))) + performed) + (cl-destructuring-bind (&key changes documentChanges) + workspace-edit + (cl-loop for change on documentChanges + do (push + (cl-destructuring-bind (&key textDocument edits) change + (cl-destructuring-bind (&key uri version) textDocument + (eglot--apply-text-edits uri edits proc version))) + performed)) + (cl-loop for (uri edits) on changes by #'cddr + do (push (eglot--apply-text-edits uri edits proc) + performed))))) + ;;; Dynamic registration ;;; From 038dd046bfde8be5f40976adf3856916afbcf5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 8 May 2018 16:07:07 +0100 Subject: [PATCH 098/771] Support workspace/applyedit * eglot.el (eglot--reply): Don't send result or error if not provided. (eglot--server-workspace/applyEdit): New server method. (eglot--apply-text-edits): Rework. (eglot--apply-workspace-edit): New helper. (eglot-rename): Simplify. --- lisp/progmodes/eglot.el | 135 ++++++++++++++++++++++++---------------- 1 file changed, 82 insertions(+), 53 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9ba87ef5bbc..37ad616a500 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -664,10 +664,10 @@ Meaning only return locally if successful, otherwise exit non-locally." (cl-defun eglot--reply (process id &key result error) "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :id id - :result result - :error error))) + (eglot--process-send + process `(:jsonrpc "2.0" :id ,id + ,@(when result `(:result ,result)) + ,@(when error `(:error ,error))))) ;;; Helpers @@ -1031,6 +1031,20 @@ running. INTERACTIVE is t if called interactively." registrations) (eglot--reply proc id :result (eglot--obj :message "OK"))))) +(cl-defun eglot--server-workspace/applyEdit + (proc &key id _label edit) + "Handle notification client/registerCapability" + (condition-case err + (progn + (eglot--apply-workspace-edit edit 'confirm) + (eglot--reply proc id :result `(:applied ))) + (error + (eglot--reply proc id + :result `(:applied :json-false) + :error + (eglot--obj :code -32001 + :message (format "%s" err)))))) + (defvar eglot--recent-before-changes nil "List of recent changes as collected by `eglot--before-change'.") (defvar eglot--recent-after-changes nil @@ -1370,61 +1384,76 @@ DUMMY is ignored" entries)) (funcall oldfun))) -(defun eglot--apply-text-edits (uri edits proc &optional version) - "Apply the EDITS for buffer of URI and return it." - (let* ((path (eglot--uri-to-path uri)) - (buffer (and path - (find-file-noselect path)))) - (unless buffer - (eglot--error "Can't find `%s' to perform server edits")) - (with-current-buffer buffer - (unless (eq proc (eglot--current-process)) - (eglot--error "Buffer `%s' for `%s' isn't managed by %s" - (current-buffer) uri proc)) - (unless (or (not version) - (equal version eglot--versioned-identifier)) - (eglot--error "Edits on `%s' require version %d, you have %d" - uri version eglot--versioned-identifier)) - (eglot--mapply - (eglot--lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (let ((start (eglot--lsp-position-to-point (plist-get range :start)))) - (goto-char start) - (delete-region start - (eglot--lsp-position-to-point (plist-get range :end))) - (insert newText))))) - edits) - (eglot--message "%s: %s edits" (current-buffer) (length edits))) - buffer)) +(defun eglot--apply-text-edits (buffer edits &optional version) + "Apply the EDITS for BUFFER." + (with-current-buffer buffer + (unless (or (not version) + (equal version eglot--versioned-identifier)) + (eglot--error "Edits on `%s' require version %d, you have %d" + buffer version eglot--versioned-identifier)) + (eglot--mapply + (eglot--lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (let ((start (eglot--lsp-position-to-point (plist-get range :start)))) + (goto-char start) + (delete-region start + (eglot--lsp-position-to-point (plist-get range :end))) + (insert newText))))) + edits) + (eglot--message "%s: Performed %s edits" (current-buffer) (length edits)))) + +(defun eglot--apply-workspace-edit (wedit &optional confirm) + "Apply the workspace edit WEDIT. If CONFIRM, ask user first." + (let (prepared) + (cl-destructuring-bind (&key changes documentChanges) + wedit + (cl-loop + for change on documentChanges + do (push (cl-destructuring-bind (&key textDocument edits) change + (cl-destructuring-bind (&key uri version) textDocument + (list (eglot--uri-to-path uri) edits version))) + prepared)) + (cl-loop for (uri edits) on changes by #'cddr + do (push (list (eglot--uri-to-path uri) edits) prepared))) + (if (or confirm + (cl-notevery #'find-buffer-visiting + (mapcar #'car prepared))) + (unless (y-or-n-p + (format "[eglot] Server requests to edit %s files.\n %s\n\ +Proceed? " + (length prepared) + (mapconcat #'identity + (mapcar #'car prepared) + "\n "))) + (eglot--error "User cancelled server edit"))) + (unwind-protect + (let (edit) + (while (setq edit (car prepared)) + (cl-destructuring-bind (path edits &optional version) edit + (eglot--apply-text-edits (find-file-noselect path) + edits + version) + (pop prepared)))) + (if prepared + (eglot--warn "Caution: edits of files %s failed." + (mapcar #'car prepared)) + (eglot--message "Edit successful!"))))) (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." (interactive - (list - (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point))))) + (list (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point))))) (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) - (let* ((proc (eglot--current-process-or-lose)) - (workspace-edit - (eglot--sync-request proc - :textDocument/rename - (append - (eglot--current-buffer-TextDocumentPositionParams) - (eglot--obj :newName newname)))) - performed) - (cl-destructuring-bind (&key changes documentChanges) - workspace-edit - (cl-loop for change on documentChanges - do (push - (cl-destructuring-bind (&key textDocument edits) change - (cl-destructuring-bind (&key uri version) textDocument - (eglot--apply-text-edits uri edits proc version))) - performed)) - (cl-loop for (uri edits) on changes by #'cddr - do (push (eglot--apply-text-edits uri edits proc) - performed))))) + (eglot--apply-workspace-edit + (eglot--sync-request (eglot--current-process-or-lose) + :textDocument/rename + (append + (eglot--current-buffer-TextDocumentPositionParams) + (eglot--obj :newName newname))) + current-prefix-arg)) ;;; Dynamic registration From 461d48a1d0414a2cc46ac565d75c791c117bd11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 May 2018 02:43:47 +0100 Subject: [PATCH 099/771] Fix odd bugs and tweak stuff * eglot.el (eglot--log-event): Insert before markers. (eglot--process-receive): Shave lines. (xref-backend-references): Use cl-return-from. (eglot--log-event): Simplify (eglot-completion-at-point): Saner annotation --- lisp/progmodes/eglot.el | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 37ad616a500..546671ee8b6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -462,7 +462,7 @@ INTERACTIVE is t if called interactively." (when interactive (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc message type) +(defun eglot--log-event (proc message &optional type) "Log an eglot-related event. PROC is the current process. MESSAGE is a JSON-like plist. TYPE is a symbol saying if this is a client or server originated." @@ -477,7 +477,7 @@ is a symbol saying if this is a client or server originated." ;; pyls keeps on sending these (t 'unexpected-thingy))) (type - (format "%s-%s" type subtype))) + (format "%s-%s" (or type :internal) subtype))) (goto-char (point-max)) (let ((msg (format "%s%s%s:\n%s\n" type @@ -486,7 +486,7 @@ is a symbol saying if this is a client or server originated." (pp-to-string message)))) (when error (setq msg (propertize msg 'face 'error))) - (insert msg))))) + (insert-before-markers msg))))) (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." @@ -500,14 +500,12 @@ is a symbol saying if this is a client or server originated." (when err (setf (eglot--status proc) `(,err t))) (cond (method ;; a server notification or a server request - (let* ((handler-sym (intern (concat "eglot--server-" - method)))) + (let* ((handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) (apply handler-sym proc (append (plist-get message :params) (if id `(:id ,id)))) - (eglot--warn "No implementation of method %s yet" - method) + (eglot--warn "No implementation of method %s yet" method) (when id (eglot--reply proc id @@ -1264,7 +1262,8 @@ DUMMY is ignored" location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) - (unless (eglot--server-capable :referencesProvider) (cl-return nil)) + (unless (eglot--server-capable :referencesProvider) + (cl-return-from xref-backend-references nil)) (let ((params (or (get-text-property 0 :textDocumentPositionParams identifier) (let ((rich (car (member identifier eglot--xref-known-symbols)))) @@ -1316,10 +1315,10 @@ DUMMY is ignored" :documentation documentation :sortText sortText)) items)))) :annotation-function - (lambda (what) (let ((detail (get-text-property 0 :detail what)) - (kind-name (get-text-property 0 :kind what))) - (concat (if detail (format " %s" detail) "") - (if kind-name (format " (%s)" kind-name) "")))) + (lambda (what) + (propertize (concat " " (or (get-text-property 0 :detail what) + (get-text-property 0 :kind what))) + 'face 'font-lock-function-name-face)) :display-sort-function (lambda (items) (sort items (lambda (a b) (string-lessp From 54a59fc000f9a3040a4644b9f25a01d9fbfac3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 May 2018 10:58:28 +0100 Subject: [PATCH 100/771] Simplify eglot--signal-textdocument/didchange * eglot.el (eglot--recent-before-changes) (eglot--recent-after-changes): Delete. (eglot--recent-changes): New var. (eglot--outstanding-edits-p, eglot--before-change) (eglot--after-change): Rewrite. (eglot--signal-textDocument/didChange): Rewrite. (eglot--signal-textDocument/didOpen): Initialize buffer-local eglot--recent-changes here. --- lisp/progmodes/eglot.el | 108 +++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 62 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 546671ee8b6..3eb43cee96d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1043,17 +1043,12 @@ running. INTERACTIVE is t if called interactively." (eglot--obj :code -32001 :message (format "%s" err)))))) -(defvar eglot--recent-before-changes nil - "List of recent changes as collected by `eglot--before-change'.") -(defvar eglot--recent-after-changes nil - "List of recent changes as collected by `eglot--after-change'.") - -(defvar-local eglot--versioned-identifier 0) - (defun eglot--current-buffer-TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) +(defvar-local eglot--versioned-identifier 0) + (defun eglot--current-buffer-VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (append (eglot--current-buffer-TextDocumentIdentifier) @@ -1077,78 +1072,67 @@ running. INTERACTIVE is t if called interactively." (eglot--obj :textDocument (eglot--current-buffer-TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) +(defvar-local eglot--recent-changes nil + "Recent buffer changes as collected by `eglot--before-change'.") + +(defun eglot--outstanding-edits-p () + "Non-nil if there are outstanding edits." + (cl-plusp (+ (length (car eglot--recent-changes)) + (length (cdr eglot--recent-changes))))) + (defun eglot--before-change (start end) "Hook onto `before-change-functions'. Records START and END, crucially convert them into LSP (line/char) positions before that information is lost (because the after-change thingy doesn't know if newlines were deleted/added)" - (push (list (eglot--pos-to-lsp-position start) - (eglot--pos-to-lsp-position end)) - eglot--recent-before-changes)) + (setf (car eglot--recent-changes) + (vconcat (car eglot--recent-changes) + `[(,(eglot--pos-to-lsp-position start) + ,(eglot--pos-to-lsp-position end))]))) (defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. Records START, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (push (list start end pre-change-length - (buffer-substring-no-properties start end)) - eglot--recent-after-changes)) + (setf (cdr eglot--recent-changes) + (vconcat (cdr eglot--recent-changes) + `[(,pre-change-length + ,(buffer-substring-no-properties start end))]))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." - (unwind-protect - (when (or eglot--recent-before-changes - eglot--recent-after-changes) - (let* ((proc (eglot--current-process-or-lose)) - (sync-kind (plist-get (eglot--capabilities proc) - :textDocumentSync)) - (emacs-messup - (/= (length eglot--recent-before-changes) - (length eglot--recent-after-changes))) - (full-sync-p (or (eq sync-kind 1) emacs-messup))) - (when emacs-messup - (unless (eq sync-kind 1) - (eglot--warn "Using full sync because before: %s and after: %s" - eglot--recent-before-changes - eglot--recent-after-changes))) - (save-restriction - (widen) - (unless (or (not sync-kind) - (eq sync-kind 0)) - (eglot--notify - proc - :textDocument/didChange - (eglot--obj - :textDocument - (eglot--current-buffer-VersionedTextDocumentIdentifier) - :contentChanges - (if full-sync-p - (vector - (eglot--obj - :text (buffer-substring-no-properties (point-min) - (point-max)))) - (apply - #'vector - (mapcar - (pcase-lambda (`(,before-start-position - ,before-end-position - ,_after-start - ,_after-end - ,len - ,after-text)) - (eglot--obj :range (eglot--obj :start before-start-position - :end before-end-position) - :rangeLength len - :text after-text)) - (reverse (cl-mapcar 'append - eglot--recent-before-changes - eglot--recent-after-changes))))))))))) - (setq eglot--recent-before-changes nil - eglot--recent-after-changes nil))) + (when (eglot--outstanding-edits-p) + (let* ((proc (eglot--current-process-or-lose)) + (sync-kind (eglot--server-capable :textDocumentSync)) + (emacs-messup (/= (length (car eglot--recent-changes)) + (length (cdr eglot--recent-changes)))) + (full-sync-p (or (eq sync-kind 1) emacs-messup))) + (when emacs-messup + (eglot--warn "`eglot--recent-changes' messup: %s" eglot--recent-changes)) + (save-restriction + (widen) + (eglot--notify + proc :textDocument/didChange + (eglot--obj + :textDocument + (eglot--current-buffer-VersionedTextDocumentIdentifier) + :contentChanges + (if full-sync-p (vector + (eglot--obj + :text (buffer-substring-no-properties (point-min) + (point-max)))) + (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) + for (len after-text) across (cdr eglot--recent-changes) + vconcat `[,(eglot--obj :range (eglot--obj :start start-pos + :end end-pos) + :rangeLength len + :text after-text)]))))) + (setq eglot--recent-changes (cons [] []))))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." + (setq eglot--recent-changes (cons [] [])) (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument From dad1b764c05ccb4e558e551067bc996a7bebd274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 May 2018 12:24:10 +0100 Subject: [PATCH 101/771] Get rid of eglot-mode * eglot.el (eglot--managed-mode): Don't call eglot-mode. When shutting down, offer to kill server. (mode-line-misc-info): Update to use eglot--managed-mode --- lisp/progmodes/eglot.el | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3eb43cee96d..bed98ed0925 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -769,7 +769,6 @@ Meaning only return locally if successful, otherwise exit non-locally." nil nil eglot-mode-map (cond (eglot--managed-mode - (eglot-mode 1) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -796,11 +795,10 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (remove-function (local imenu-create-index-function) #'eglot-imenu)))) - -(define-minor-mode eglot-mode - "Minor mode for all buffers managed by EGLOT in some way." nil - nil eglot-mode-map) + (remove-function (local imenu-create-index-function) #'eglot-imenu) + (let ((proc (eglot--current-process))) + (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) + (eglot-shutdown proc nil t)))))) (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer is managed by PROC." @@ -886,7 +884,8 @@ Uses THING, FACE, DEFS and PREPEND." (mouse-3 eglot-clear-status "clear this status")) (format "%d pending requests\n" pending))))))))) -(add-to-list 'mode-line-misc-info `(eglot-mode (" [" eglot--mode-line-format "] "))) +(add-to-list 'mode-line-misc-info + `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) ;;; Protocol implementation (Requests, notifications, etc) From 0aa29932a60ea837cf226da55ce3b984859af8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 May 2018 22:26:02 +0100 Subject: [PATCH 102/771] Simplify `eglot-shutdown` * eglot.el (eglot, eglot-reconnect) (eglot--managed-mode): Call new eglot-shutdown. (eglot-shutdown): Simplify. (eglot--process-sentinel): Also call error functions. (eglot--process-filter): Reindent. --- lisp/progmodes/eglot.el | 146 ++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 80 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bed98ed0925..0795bc3e8c9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -310,7 +310,7 @@ INTERACTIVE is t if called interactively." (y-or-n-p "[eglot] Live process found, reconnect instead? ")) (eglot-reconnect current-process interactive) (when (process-live-p current-process) - (eglot-shutdown current-process 'sync)) + (eglot-shutdown current-process)) (eglot--connect project managed-major-mode short-name @@ -327,7 +327,7 @@ managing `%s' buffers in project `%s'." INTERACTIVE is t if called interactively." (interactive (list (eglot--current-process-or-lose) t)) (when (process-live-p process) - (eglot-shutdown process 'sync interactive)) + (eglot-shutdown process interactive)) (eglot--connect (eglot--project process) (eglot--major-mode process) (eglot--short-name process) @@ -341,13 +341,12 @@ INTERACTIVE is t if called interactively." "Called with PROCESS undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) (when (not (process-live-p process)) - ;; Remember to cancel all timers + ;; Cancel timers and error any outstanding continuations ;; - (maphash (lambda (id triplet) - (cl-destructuring-bind (_success _error timeout) triplet - (eglot--message - "(sentinel) Cancelling timer for continuation %s" id) - (cancel-timer timeout))) + (maphash (lambda (_id triplet) + (cl-destructuring-bind (_success error timeout) triplet + (cancel-timer timeout) + (funcall error :code -1 :message (format "Server died")))) (eglot--pending-continuations process)) ;; Turn off `eglot--managed-mode' where appropriate. ;; @@ -400,46 +399,47 @@ INTERACTIVE is t if called interactively." (unwind-protect (catch done (while t - (cond ((not expected-bytes) - ;; Starting a new message - ;; - (setq expected-bytes - (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \ + (cond + ((not expected-bytes) + ;; Starting a new message + ;; + (setq expected-bytes + (and (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \ *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t) - (string-to-number (match-string 1)))) - (unless expected-bytes - (throw done :waiting-for-new-message))) - (t - ;; Attempt to complete a message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; Process content in another buffer, - ;; shielding buffer from tamper - ;; - (with-temp-buffer - (eglot--process-receive proc json-message)))) - (goto-char message-end) - (delete-region (point-min) (point)) - (setq expected-bytes nil)))) - (t - ;; Message is still incomplete - ;; - (throw done :waiting-for-more-bytes-in-this-message)))))))) + (+ (point) 100) + t) + (string-to-number (match-string 1)))) + (unless expected-bytes + (throw done :waiting-for-new-message))) + (t + ;; Attempt to complete a message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; Process content in another buffer, + ;; shielding buffer from tamper + ;; + (with-temp-buffer + (eglot--process-receive proc json-message)))) + (goto-char message-end) + (delete-region (point-min) (point)) + (setq expected-bytes nil)))) + (t + ;; Message is still incomplete + ;; + (throw done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; (setf (eglot--expected-bytes proc) expected-bytes)))))) @@ -798,7 +798,7 @@ Meaning only return locally if successful, otherwise exit non-locally." (remove-function (local imenu-create-index-function) #'eglot-imenu) (let ((proc (eglot--current-process))) (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) - (eglot-shutdown proc nil t)))))) + (eglot-shutdown proc t)))))) (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer is managed by PROC." @@ -890,38 +890,24 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (process &optional sync interactive) - "Politely ask the server PROCESS to quit. -Forcefully quit it if it doesn't respond. -If SYNC, don't leave this function with the server still -running. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t t)) - (when interactive - (eglot--message "(eglot-shutdown) Asking %s politely to terminate" - process)) - (let ((brutal (lambda () - (eglot--warn "Brutally deleting existing process %s" - process) - (setf (eglot--moribund process) t) - (delete-process process)))) - (eglot--request - process :shutdown nil - :success-fn (lambda (&rest _anything) - (when interactive - (eglot--message "Now asking %s politely to exit" process)) - (setf (eglot--moribund process) t) - (eglot--request process - :exit - nil - :success-fn brutal - :async-p (not sync) - :error-fn brutal - :timeout-fn brutal - :timeout 3)) - :error-fn brutal - :async-p (not sync) - :timeout-fn brutal - :timeout 3))) +(defun eglot-shutdown (proc &optional interactive) + "Politely ask the server PROC to quit. +Forcefully quit it if it doesn't respond. Don't leave this +function with the server still running. INTERACTIVE is t if +called interactively." + (interactive (list (eglot--current-process-or-lose) t)) + (when interactive (eglot--message "Asking %s politely to terminate" proc)) + (unwind-protect + (let ((eglot-request-timeout 3)) + (setf (eglot--moribund proc) t) + (eglot--sync-request proc + :shutdown + nil) + ;; this one should always fail + (ignore-errors (eglot--sync-request proc :exit nil))) + (when (process-live-p proc) + (eglot--warn "Brutally deleting existing process %s" proc) + (delete-process proc)))) (cl-defun eglot--server-window/showMessage (_process &key type message) "Handle notification window/showMessage" From 0a9c14efad60e29ba7604e19f5822757e0c066d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 May 2018 22:28:29 +0100 Subject: [PATCH 103/771] Call eglot-eldoc-function after completion finishes * eglot.el (eglot-completion-at-point): Call eglot-eldoc-function after completion finishes. --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0795bc3e8c9..d5eae031e08 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1292,7 +1292,9 @@ DUMMY is ignored" (lambda (items) (sort items (lambda (a b) (string-lessp (get-text-property 0 :sortText a) - (get-text-property 0 :sortText b))))))))) + (get-text-property 0 :sortText b))))) + :exit-function + (lambda (_string _status) (eglot-eldoc-function)))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") From d76cc9aea9c0e77bac28385050f14acff4b4e25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 May 2018 22:41:37 +0100 Subject: [PATCH 104/771] New "deferred requests" that wait until server is ready Calling textDocument/hover or textDocument/documentHighlight before the server has had a chance to process a textDocument/didChange is normally useless. The matter is worse for servers like RLS which only become ready much later and send a special notif for it (see https://github.com/rust-lang-nursery/rls/issues/725). So, keeping the same coding style add a DEFERRED arg to eglot--request that makes it maybe not run the function immediately. Add a bunch of logic for probing readiness of servers. * README.md: Update * eglot.el (eglot--deferred-actions): New process-local var. (eglot--process-filter): Call deferred actions. (eglot--request): Rewrite. (eglot--sync-request): Rewrite. (eglot--call-deferred, eglot--ready-predicates) (eglot--server-ready-p): New helpers. (eglot--signal-textDocument/didChange): Set spinner and call deferred actions. (eglot-completion-at-point): Pass DEFERRED to eglot-sync-request. (eglot-eldoc-function): Pass DEFERRED to eglot-request (eglot--rls-probably-ready-for-p): New helper. (rust-mode-hook): Add eglot--setup-rls-idiosyncrasies (eglot--setup-rls-idiosyncrasies): New helper. --- lisp/progmodes/eglot.el | 237 +++++++++++++++++++++++----------------- 1 file changed, 134 insertions(+), 103 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d5eae031e08..9a0b8246c1b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -135,6 +135,10 @@ A list (WHAT SERIOUS-P)." t) Either a list of strings (a shell command and arguments), or a list of a single string of the form :") +(eglot--define-process-var eglot--deferred-actions + (make-hash-table :test #'equal) + "Actions deferred to when server is thought to be ready.") + (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is a name to give the inferior process or connection. @@ -442,7 +446,8 @@ INTERACTIVE is t if called interactively." (throw done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; - (setf (eglot--expected-bytes proc) expected-bytes)))))) + (setf (eglot--expected-bytes proc) expected-bytes)))) + (eglot--call-deferred proc))) (defun eglot-events-buffer (process &optional interactive) "Display events buffer for current LSP connection PROCESS. @@ -549,110 +554,118 @@ is a symbol saying if this is a client or server originated." (interactive (list (eglot--current-process-or-lose))) (setf (eglot--status process) nil)) -(cl-defun eglot--request (process - method - params - &key success-fn error-fn timeout-fn (async-p t) - (timeout eglot-request-timeout)) - "Make a request to PROCESS, expecting a reply. -Return the ID of this request, unless ASYNC-P is nil, in which -case never returns locally. Wait TIMEOUT seconds for a -response." - (let* ((id (eglot--next-request-id)) - (timeout-fn (or timeout-fn - (lambda () - (eglot--warn - "(request) Tired of waiting for reply to %s" id)))) - (error-fn (or error-fn - (cl-function - (lambda (&key code message &allow-other-keys) - (setf (eglot--status process) `(,message t)) - (eglot--warn - "(request) Request id=%s errored with code=%s: %s" - id code message))))) - (success-fn (or success-fn - (cl-function - (lambda (&rest result-body) - (eglot--debug - "(request) Request id=%s replied to with result=%s" - id result-body))))) - (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) - (eglot--process-send process - (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)) - (catch catch-tag - (let ((timeout-timer - (run-with-timer - timeout nil - (if async-p - (lambda () - (remhash id (eglot--pending-continuations process)) - (funcall timeout-fn)) - (lambda () - (remhash id (eglot--pending-continuations process)) - (throw catch-tag (funcall timeout-fn))))))) - (puthash id - (list (if async-p - success-fn - (lambda (&rest args) - (throw catch-tag (apply success-fn args)))) - (if async-p - error-fn - (lambda (&rest args) - (throw catch-tag (apply error-fn args)))) - timeout-timer) - (eglot--pending-continuations process)) - (unless async-p - (unwind-protect - (while t - (unless (process-live-p process) - (cond ((eglot--moribund process) - (throw catch-tag (delete-process process))) - (t - (eglot--error - "(request) Proc %s died unexpectedly during request with code %s" - process - (process-exit-status process))))) - (accept-process-output nil 0.01)) - (when (memq timeout-timer timer-list) - (eglot--message - "(request) Last-change cancelling timer for continuation %s" id) - (cancel-timer timeout-timer)))))) - ;; Finally, return the id. - id)) +(defun eglot--call-deferred (proc) + "Call PROC's deferred actions, who may again defer themselves." + (let ((actions (hash-table-values (eglot--deferred-actions proc)))) + (eglot--log-event proc `(:running-deferred ,(length actions))) + (mapc #'funcall (mapcar #'car actions)))) + +(defvar eglot--ready-predicates '(eglot--server-ready-p) + "Special hook of predicates controlling deferred actions. +When one of these functions returns nil, a deferrable +`eglot--request' will be deferred. Each predicate is passed the +an symbol for the request request and a process object.") + +(defun eglot--server-ready-p (_what _proc) + "Tell if server of PROC ready for processing deferred WHAT." + (not (eglot--outstanding-edits-p))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) (declare (indent 1) (debug (sexp &rest form))) `(cl-function (lambda ,cl-lambda-list ,@body))) -(defun eglot--sync-request (proc method params) +(cl-defun eglot--request (proc + method + params + &rest args + &key success-fn error-fn timeout-fn + (timeout eglot-request-timeout) + (deferred nil)) + "Make a request to PROCESS, expecting a reply. +Return the ID of this request. Wait TIMEOUT seconds for response. +If DEFERRED, maybe defer request to the future, or never at all, +in case a new request with identical DEFERRED and for the same +buffer overrides it. However, if that happens, the original +timeout keeps counting." + (let* ((id (eglot--next-request-id)) + (existing-timer nil) + (make-timeout + (lambda ( ) + (or existing-timer + (run-with-timer + timeout nil + (lambda () + (remhash id (eglot--pending-continuations proc)) + (funcall (or timeout-fn + (lambda () + (eglot--error + "Tired of waiting for reply to %s, id=%s" + method id)))))))))) + (when deferred + (let* ((buf (current-buffer)) + (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) + (when existing (setq existing-timer (cadr existing))) + (if (run-hook-with-args-until-failure 'eglot--ready-predicates + deferred proc) + (remhash (list deferred buf) (eglot--deferred-actions proc)) + (eglot--log-event proc `(:deferring ,method :id ,id :params ,params)) + (let* ((buf (current-buffer)) (point (point)) + (later (lambda () + (when (buffer-live-p buf) + (with-current-buffer buf + (save-excursion (goto-char point) + (apply #'eglot--request proc + method params args))))))) + (puthash (list deferred buf) (list later (funcall make-timeout)) + (eglot--deferred-actions proc)) + (cl-return-from eglot--request nil))))) + ;; Really run it + ;; + (puthash id + (list (or success-fn (eglot--lambda (&rest result-body) + (eglot--debug + "Request %s, id=%s replied to with result=%s" + method id result-body))) + (or error-fn (eglot--lambda + (&key code message &allow-other-keys) + (setf (eglot--status proc) `(,message t)) + (eglot--warn + "Request %s, id=%s errored with code=%s: %s" + method id code message))) + (funcall make-timeout)) + (eglot--pending-continuations proc)) + (eglot--process-send proc (eglot--obj :jsonrpc "2.0" + :id id + :method method + :params params)))) + +(defun eglot--sync-request (proc method params &optional deferred) "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. -Meaning only return locally if successful, otherwise exit non-locally." - (let* ((timeout-error-sym (cl-gensym)) - (catch-tag (make-symbol "eglot--sync-request-catch-tag")) - (retval - (catch catch-tag - (eglot--request proc method params - :success-fn (lambda (&rest args) - (throw catch-tag (if (vectorp (car args)) - (car args) - args))) - :error-fn (eglot--lambda - (&key code message &allow-other-keys) - (eglot--error "Oops: %s: %s" code message)) - :timeout-fn (lambda () - (throw catch-tag timeout-error-sym)) - :async-p nil)))) - ;; FIXME: There's maybe an emacs bug here. Because timeout-fn runs - ;; in a timer, the better and obvious choice of throwing the erro - ;; in the lambda is not quitting the `accept-process-output' - ;; infinite loop up there. So use this contorted strategy with - ;; `cl-gensym'. - (if (eq retval timeout-error-sym) - (eglot--error "Tired of waiting for reply to sync request") - retval))) +Meaning only return locally if successful, otherwise exit non-locally. +DEFERRED is passed to `eglot--request', which see." + ;; Launching a deferred sync request with outstanding changes is a + ;; bad idea, since that might lead to the request never having a + ;; chance to run, because `eglot--ready-predicates'. + (when deferred (eglot--signal-textDocument/didChange)) + (let* ((done (make-symbol "eglot--sync-request-catch-tag")) + (res + (catch done (eglot--request + proc method params + :success-fn (lambda (&rest args) + (throw done (if (vectorp (car args)) + (car args) args))) + :error-fn (eglot--lambda + (&key code message &allow-other-keys) + (throw done + `(error ,(format "Oops: %s: %s" + code message)))) + :timeout-fn (lambda () + (throw done '(error "Timed out"))) + :deferred deferred) + ;; now spin, baby! + (while t (accept-process-output nil 0.01))))) + (when (and (listp res) (eq 'error (car res))) (eglot--error (cadr res))) + res)) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" @@ -1113,7 +1126,9 @@ Records START, END and PRE-CHANGE-LENGTH locally." :end end-pos) :rangeLength len :text after-text)]))))) - (setq eglot--recent-changes (cons [] []))))) + (setq eglot--recent-changes (cons [] [])) + (setf (eglot--spinner proc) (list nil :textDocument/didChange t)) + (eglot--call-deferred proc)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." @@ -1273,7 +1288,8 @@ DUMMY is ignored" (let* ((resp (eglot--sync-request proc :textDocument/completion - (eglot--current-buffer-TextDocumentPositionParams))) + (eglot--current-buffer-TextDocumentPositionParams) + :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail @@ -1311,7 +1327,8 @@ DUMMY is ignored" (if (vectorp contents) contents (list contents)) - "\n"))))) + "\n"))) + :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--request proc :textDocument/documentHighlight position-params @@ -1331,7 +1348,8 @@ DUMMY is ignored" (overlay-put ov 'evaporate t) (overlay-put ov :kind kind) ov))) - highlights)))))))) + highlights))))) + :deferred :textDocument/documentHighlight))) nil) (defun eglot-imenu (oldfun) @@ -1438,6 +1456,19 @@ Proceed? " ;;; Rust-specific ;;; +(defun eglot--rls-probably-ready-for-p (what proc) + "Guess if the RLS running in PROC is ready for WHAT." + (or (eq what :textDocument/completion) ; RLS normally ready for this + ; one, even if building + (pcase-let ((`(,_id ,what ,done) (eglot--spinner proc))) + (and (equal "Indexing" what) done)))) + +(add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) + +(defun eglot--setup-rls-idiosyncrasies () + "RLS needs special treatment..." + (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t)) + (cl-defun eglot--server-window/progress (process &key id done title &allow-other-keys) "Handle notification window/progress" From 040d3e78ea381c79829c031642c4cafdee6b1961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 May 2018 23:12:19 +0100 Subject: [PATCH 105/771] Fancier rls spinner * eglot.el (eglot--mode-line-format): Use (nth 3) of eglot--spinner. (eglot--server-window/progress): Save detail message in spinner. --- lisp/progmodes/eglot.el | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9a0b8246c1b..4739cea72f1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -869,7 +869,7 @@ Uses THING, FACE, DEFS and PREPEND." (name (and (process-live-p proc) (eglot--short-name proc))) (pending (and proc (hash-table-count (eglot--pending-continuations proc)))) - (`(,_id ,doing ,done-p) (and proc (eglot--spinner proc))) + (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) (`(,status ,serious-p) (and proc (eglot--status proc)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line @@ -888,7 +888,9 @@ Uses THING, FACE, DEFS and PREPEND." (format "An error occured: %s\n" status)))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props - doing 'compilation-mode-line-run + (format "%s%s" doing + (if detail (format ":%s" detail) "")) + 'compilation-mode-line-run '((mouse-1 eglot-events-buffer "go to events buffer"))))) ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props @@ -1460,7 +1462,7 @@ Proceed? " "Guess if the RLS running in PROC is ready for WHAT." (or (eq what :textDocument/completion) ; RLS normally ready for this ; one, even if building - (pcase-let ((`(,_id ,what ,done) (eglot--spinner proc))) + (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner proc))) (and (equal "Indexing" what) done)))) (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) @@ -1470,9 +1472,9 @@ Proceed? " (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t)) (cl-defun eglot--server-window/progress - (process &key id done title &allow-other-keys) + (process &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner process) (list id title done))) + (setf (eglot--spinner process) (list id title done message))) (provide 'eglot) ;;; eglot.el ends here From d2fa8fea9aa71f9ef553b2a67946a33f208e5cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 May 2018 23:24:15 +0100 Subject: [PATCH 106/771] Add minimal headers, commentary and autoloads --- lisp/progmodes/eglot.el | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4739cea72f1..eba0e84f267 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,8 +2,11 @@ ;; Copyright (C) 2017 João Távora +;; Version: 0.1 ;; Author: João Távora -;; Keywords: extensions +;; Url: https://github.com/joaotavora/eglot +;; Keywords: convenience, languages +;; Package-Requires: ((emacs "26.1")) ;; 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 @@ -20,7 +23,8 @@ ;;; Commentary: -;; +;; M-x eglot in some file under some .git controlled dir should get +;; you started, but see README.md. ;;; Code: @@ -285,6 +289,7 @@ Enter program to execute (or :): " guessed-command)) t))) +;;;###autoload (defun eglot (managed-major-mode command &optional interactive) "Start a Language Server Protocol server. Server is started with COMMAND and manages buffers of @@ -1465,8 +1470,10 @@ Proceed? " (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner proc))) (and (equal "Indexing" what) done)))) +;;;###autoload (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) +;;;###autoload (defun eglot--setup-rls-idiosyncrasies () "RLS needs special treatment..." (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t)) From eac9d2917847f77431bdffefae751c99a15e1df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 12:07:11 +0100 Subject: [PATCH 107/771] More correctly setup rust-mode-related autoloads By autoloading the add-hook form and the eglot--setup-rls-idiosyncrasies definition, a user can start rust-mode without loading eglot.el along with it. * eglot.el (rust-mode-hook) (eglot--setup-rls-idiosyncrasies): Wrap in autoloaded progn. --- lisp/progmodes/eglot.el | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eba0e84f267..fbddf16c3f2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1471,12 +1471,11 @@ Proceed? " (and (equal "Indexing" what) done)))) ;;;###autoload -(add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) - -;;;###autoload -(defun eglot--setup-rls-idiosyncrasies () - "RLS needs special treatment..." - (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t)) +(progn + (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) + (defun eglot--setup-rls-idiosyncrasies () + "Prepare `eglot' to deal with RLS's special treatment." + (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) (cl-defun eglot--server-window/progress (process &key id done title message &allow-other-keys) From dd4fada6b54b5f72d1a8a4852f386e57078a4b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 12:07:31 +0100 Subject: [PATCH 108/771] Shorten summary line to appease package-lint.el --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fbddf16c3f2..21ec247967a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,4 +1,4 @@ -;;; eglot.el --- A client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- +;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- ;; Copyright (C) 2017 João Távora From 4d5eff8c97a21d8bb245fe7f3b9d68efbd29d866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 12:21:29 +0100 Subject: [PATCH 109/771] Adjust flymake integration When opening a new file (signalling textDocument/didOpen) it makes sense to call the flymake callback (if it exists) with no diagnostics, just to get rid of that "Wait", since we don't know if later in this callback cycle the server will ever report new diagnostics. * eglot.el (eglot--managed-mode): Don't call flymake-mode or eldoc-mode (eglot--managed-mode-hook): Add them here. (eglot--maybe-activate-editing-mode): Call flymake callback. (eglot--server-textDocument/publishDiagnostics): Set unreported diagnostics to nil if invoking callback. --- lisp/progmodes/eglot.el | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 21ec247967a..f865c22589e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -798,9 +798,7 @@ DEFERRED is passed to `eglot--request', which see." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (add-function :around (local imenu-create-index-function) #'eglot-imenu) - (flymake-mode 1) - (eldoc-mode 1)) + (add-function :around (local imenu-create-index-function) #'eglot-imenu)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) @@ -818,6 +816,9 @@ DEFERRED is passed to `eglot--request', which see." (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) (eglot-shutdown proc t)))))) +(add-hook 'eglot--managed-mode-hook 'flymake-mode) +(add-hook 'eglot--managed-mode-hook 'eldoc-mode) + (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer is managed by PROC." (and buffer-file-name (let ((cur (eglot--current-process))) @@ -832,7 +833,8 @@ that case, also signal textDocument/didOpen." (when (eglot--buffer-managed-p proc) (eglot--managed-mode 1) (eglot--signal-textDocument/didOpen) - (flymake-start))) + (flymake-start) + (funcall (or eglot--current-flymake-report-fn #'ignore) nil))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -999,11 +1001,11 @@ called interactively." (t :note)) (concat source ": " message))))) into diags - finally (if eglot--current-flymake-report-fn - (funcall eglot--current-flymake-report-fn - diags) - (setq eglot--unreported-diagnostics - diags))))) + finally (cond (eglot--current-flymake-report-fn + (funcall eglot--current-flymake-report-fn diags) + (setq eglot--unreported-diagnostics nil)) + (t + (setq eglot--unreported-diagnostics diags)))))) (t (eglot--message "OK so %s isn't visited" filename))))) @@ -1174,15 +1176,12 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. Calls REPORT-FN maybe if server publishes diagnostics in time." - ;; Maybe call immediately if anything unreported (this will clear - ;; any pending diags) + (setq eglot--current-flymake-report-fn report-fn) + ;; Report anything unreported (when eglot--unreported-diagnostics (funcall report-fn eglot--unreported-diagnostics) (setq eglot--unreported-diagnostics nil)) - ;; Setup so maybe it's called later, too. - (setq eglot--current-flymake-report-fn report-fn) - ;; Take this opportunity to signal a didChange that might eventually - ;; make the server report new diagnostics. + ;; Signal a didChange that might eventually bring new diagnotics (eglot--signal-textDocument/didChange)) (defun eglot-xref-backend () From 845063c090678d5888f69ec58baebe215b26f885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 12:51:21 +0100 Subject: [PATCH 110/771] More rls-specifics: update flymake diags when indexing done RLS could/should report diagnostics for every opened file, even if there aren't any problems. Because it doesn't, loop for every buffer managed by the process and call eglot--current-flymake-report-fn * eglot.el (eglot--server-window/progress): Call eglot--current-flymake-report-fn --- lisp/progmodes/eglot.el | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f865c22589e..1ba5324de93 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -825,6 +825,9 @@ DEFERRED is passed to `eglot--request', which see." (or (and (null proc) cur) (and proc (eq proc cur)))))) +(defvar-local eglot--current-flymake-report-fn nil + "Current flymake report function for this buffer") + (defun eglot--maybe-activate-editing-mode (&optional proc) "Maybe activate mode function `eglot--managed-mode'. If PROC is supplied, do it only if BUFFER is managed by it. In @@ -969,9 +972,6 @@ called interactively." "Handle notification telemetry/event" (eglot--log "Server telemetry: %s" any)) -(defvar-local eglot--current-flymake-report-fn nil - "Current flymake report function for this buffer") - (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") @@ -1479,7 +1479,13 @@ Proceed? " (cl-defun eglot--server-window/progress (process &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner process) (list id title done message))) + (setf (eglot--spinner process) (list id title done message)) + (when (and (equal "Indexing" title) done) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (eglot--buffer-managed-p process) + (funcall (or eglot--current-flymake-report-fn #'ignore) + eglot--unreported-diagnostics)))))) (provide 'eglot) ;;; eglot.el ends here From f0d4e043b130d9dfaf125957194e4c75111f0773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 12:54:35 +0100 Subject: [PATCH 111/771] Simplify mode-line updating logic * eglot.el (eglot--define-process-var): Simplify. (eglot--short-name, eglot--spinner, eglot--status): Don't auto-update mode-line. (eglot--process-receive): Update it here. --- lisp/progmodes/eglot.el | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1ba5324de93..bf0b9e9271e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -78,11 +78,9 @@ (if (project-current) "" " (Also no current project)")))) (defmacro eglot--define-process-var - (var-sym initval &optional doc mode-line-update-p) + (var-sym initval &optional doc) "Define VAR-SYM as a generalized process-local variable. -INITVAL is the default value. DOC is the documentation. -MODE-LINE-UPDATE-P says to also force a mode line update -after setting it." +INITVAL is the default value. DOC is the documentation." (declare (indent 2)) `(progn (put ',var-sym 'function-documentation ,doc) @@ -94,16 +92,11 @@ after setting it." (let ((def ,initval)) (process-put proc ',var-sym def) def)))) - (gv-define-setter ,var-sym (to-store &optional process) - (let* ((prop ',var-sym)) - ,(let ((form '(let ((proc (or ,process (eglot--current-process-or-lose)))) - (process-put proc ',prop ,to-store)))) - (if mode-line-update-p - `(backquote (prog1 ,form (force-mode-line-update t))) - `(backquote ,form))))))) + (gv-define-setter ,var-sym (to-store process) + `(let ((once ,to-store)) (process-put ,process ',',var-sym once) once)))) (eglot--define-process-var eglot--short-name nil - "A short name for the process" t) + "A short name for the process") (eglot--define-process-var eglot--major-mode nil "The major-mode this server is managing.") @@ -128,11 +121,11 @@ after setting it." (eglot--define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. -A list (ID WHAT DONE-P)." t) +A list (ID WHAT DONE-P).") (eglot--define-process-var eglot--status `(:unknown nil) "Status as declared by the server. -A list (WHAT SERIOUS-P)." t) +A list (WHAT SERIOUS-P).") (eglot--define-process-var eglot--contact nil "Method used to contact a server. @@ -531,7 +524,8 @@ is a symbol saying if this is a client or server originated." (apply (cl-first continuations) res) (funcall (cl-first continuations) res))))) (id - (eglot--warn "Ooops no continuation for id %s" id))))) + (eglot--warn "Ooops no continuation for id %s" id))) + (force-mode-line-update t))) (defvar eglot--expect-carriage-return nil) From 8fb14037db4849c0f06f4fda458f845826fa78ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 13:14:19 +0100 Subject: [PATCH 112/771] Resist server failure during synchronous requests Calling the error handler unprotected could lead to the rest of the sentinel not running at all. This defeated the auto-reconnection in Rust, for example. * eglot.el (eglot--process-sentinel): Rework. --- lisp/progmodes/eglot.el | 81 ++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bf0b9e9271e..d49367b5fef 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -339,50 +339,47 @@ INTERACTIVE is t if called interactively." (defvar eglot--inhibit-auto-reconnect nil "If non-nil, don't autoreconnect on unexpected quit.") -(defun eglot--process-sentinel (process change) - "Called with PROCESS undergoes CHANGE." +(defun eglot--process-sentinel (proc change) + "Called when PROC undergoes CHANGE." (eglot--debug "(sentinel) Process state changed to %s" change) - (when (not (process-live-p process)) - ;; Cancel timers and error any outstanding continuations - ;; + (when (not (process-live-p proc)) + ;; Cancel outstanding timers (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success error timeout) triplet - (cancel-timer timeout) - (funcall error :code -1 :message (format "Server died")))) - (eglot--pending-continuations process)) - ;; Turn off `eglot--managed-mode' where appropriate. - ;; - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (when (eglot--buffer-managed-p process) - (eglot--managed-mode -1)))) - ;; Forget about the process-project relationship - ;; - (setf (gethash (eglot--project process) eglot--processes-by-project) - (delq process - (gethash (eglot--project process) eglot--processes-by-project))) - (cond ((eglot--moribund process) - (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status process))) - ((null eglot--inhibit-auto-reconnect) - (eglot--warn - "(sentinel) Reconnecting after process unexpectedly changed to `%s'." - change) - (condition-case-unless-debug err - (eglot-reconnect process) - (error (eglot--warn "Auto-reconnect failed: %s " err) )) - (setq eglot--inhibit-auto-reconnect - (run-with-timer - 3 nil - (lambda () - (setq eglot--inhibit-auto-reconnect nil))))) - (t - (eglot--warn - "(sentinel) Not auto-reconnecting, last one didn't last long." - change))) - (force-mode-line-update t) - (delete-process process))) - + (cl-destructuring-bind (_success _error timeout) triplet + (cancel-timer timeout))) + (eglot--pending-continuations proc)) + (unwind-protect + ;; Call all outstanding error handlers + (maphash (lambda (_id triplet) + (cl-destructuring-bind (_success error _timeout) triplet + (funcall error :code -1 :message (format "Server died")))) + (eglot--pending-continuations proc)) + ;; Turn off `eglot--managed-mode' where appropriate. + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (eglot--buffer-managed-p proc) + (eglot--managed-mode -1)))) + ;; Forget about the process-project relationship + (setf (gethash (eglot--project proc) eglot--processes-by-project) + (delq proc + (gethash (eglot--project proc) eglot--processes-by-project))) + (cond ((eglot--moribund proc) + (eglot--message "(sentinel) Moribund process exited with status %s" + (process-exit-status proc))) + ((null eglot--inhibit-auto-reconnect) + (eglot--warn + "(sentinel) Reconnecting after process unexpectedly changed to `%s'." + change) + (setq eglot--inhibit-auto-reconnect + (run-with-timer 3 nil + (lambda () + (setq eglot--inhibit-auto-reconnect nil)))) + (eglot-reconnect proc)) + (t + (eglot--warn + "(sentinel) Not auto-reconnecting, last one didn't last long." + change))) + (delete-process proc)))) (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." (when (buffer-live-p (process-buffer proc)) From 68892622c9da9f16dbd12b11486680b0e8038844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 13:52:21 +0100 Subject: [PATCH 113/771] Only call deferred actions after a full message has been received Otherwise it can be quite wasteful. * eglot.el (eglot--process-filter): Don't eglot--call-deferred here. (eglot--process-receive): Do it here. --- lisp/progmodes/eglot.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d49367b5fef..8b8d7122444 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -380,6 +380,7 @@ INTERACTIVE is t if called interactively." "(sentinel) Not auto-reconnecting, last one didn't last long." change))) (delete-process proc)))) + (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." (when (buffer-live-p (process-buffer proc)) @@ -441,8 +442,7 @@ INTERACTIVE is t if called interactively." (throw done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; - (setf (eglot--expected-bytes proc) expected-bytes)))) - (eglot--call-deferred proc))) + (setf (eglot--expected-bytes proc) expected-bytes)))))) (defun eglot-events-buffer (process &optional interactive) "Display events buffer for current LSP connection PROCESS. @@ -522,6 +522,7 @@ is a symbol saying if this is a client or server originated." (funcall (cl-first continuations) res))))) (id (eglot--warn "Ooops no continuation for id %s" id))) + (eglot--call-deferred proc) (force-mode-line-update t))) (defvar eglot--expect-carriage-return nil) From 979a90456d89da9d838d79ff0b727e4f24e0305a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 18:44:47 +0100 Subject: [PATCH 114/771] Reduce log chatter * eglot.el (eglot--process-sentinel, eglot--request): Use eglot--log-event. (eglot--log-event): Print "message" if type unknown. (eglot--debug, eglot--log): Remove. (eglot--server-window/logMessage, eglot--server-telemetry/event): Make noops. (eglot--call-deferred): Also reduce chatter here. --- lisp/progmodes/eglot.el | 48 ++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8b8d7122444..d1e4d1548a4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -341,7 +341,7 @@ INTERACTIVE is t if called interactively." (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." - (eglot--debug "(sentinel) Process state changed to %s" change) + (eglot--log-event proc `(:message "Process state changed" :change ,change)) (when (not (process-live-p proc)) ;; Cancel outstanding timers (maphash (lambda (_id triplet) @@ -474,8 +474,7 @@ is a symbol saying if this is a client or server originated." (subtype (cond ((and method id) 'request) (method 'notification) (id 'reply) - ;; pyls keeps on sending these - (t 'unexpected-thingy))) + (t 'message))) (type (format "%s-%s" (or type :internal) subtype))) (goto-char (point-max)) @@ -553,7 +552,7 @@ is a symbol saying if this is a client or server originated." (defun eglot--call-deferred (proc) "Call PROC's deferred actions, who may again defer themselves." - (let ((actions (hash-table-values (eglot--deferred-actions proc)))) + (when-let ((actions (hash-table-values (eglot--deferred-actions proc)))) (eglot--log-event proc `(:running-deferred ,(length actions))) (mapc #'funcall (mapcar #'car actions)))) @@ -619,16 +618,15 @@ timeout keeps counting." ;; Really run it ;; (puthash id - (list (or success-fn (eglot--lambda (&rest result-body) - (eglot--debug - "Request %s, id=%s replied to with result=%s" - method id result-body))) - (or error-fn (eglot--lambda - (&key code message &allow-other-keys) - (setf (eglot--status proc) `(,message t)) - (eglot--warn - "Request %s, id=%s errored with code=%s: %s" - method id code message))) + (list (or success-fn + (eglot--lambda (&rest _ignored) + (eglot--log-event + proc (eglot--obj :message "success ignored" :id id)))) + (or error-fn + (eglot--lambda (&key code message &allow-other-keys) + (setf (eglot--status proc) `(,message t)) + proc (eglot--obj :message "error ignored, status set" + :id id :error code))) (funcall make-timeout)) (eglot--pending-continuations proc)) (eglot--process-send proc (eglot--obj :jsonrpc "2.0" @@ -680,12 +678,6 @@ DEFERRED is passed to `eglot--request', which see." ;;; Helpers ;;; -(defun eglot--debug (format &rest args) - "Debug message FORMAT with ARGS." - (display-warning 'eglot - (apply #'format format args) - :debug)) - (defun eglot--error (format &rest args) "Error out with FORMAT with ARGS." (error (apply #'format format args))) @@ -694,10 +686,6 @@ DEFERRED is passed to `eglot--request', which see." "Message out with FORMAT with ARGS." (message (concat "[eglot] " (apply #'format format args)))) -(defun eglot--log (format &rest args) - "Log out with FORMAT with ARGS." - (message (concat "[eglot-log] " (apply #'format format args)))) - (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." (apply #'eglot--message (concat "(warning) " format) args) @@ -954,15 +942,11 @@ called interactively." :error (eglot--obj :code -32800 :message "User cancelled")))))) -(cl-defun eglot--server-window/logMessage (_process &key type message) - "Handle notification window/logMessage" - (eglot--log (propertize "Server reports (type=%s): %s" - 'face (if (<= type 1) 'error)) - type message)) +(cl-defun eglot--server-window/logMessage (_proc &key _type _message) + "Handle notification window/logMessage") ;; noop, use events buffer -(cl-defun eglot--server-telemetry/event (_process &rest any) - "Handle notification telemetry/event" - (eglot--log "Server telemetry: %s" any)) +(cl-defun eglot--server-telemetry/event (_proc &rest _any) + "Handle notification telemetry/event") ;; noop, use events buffer (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") From 522bcdf0e82546920a74239f2eeadae3af5921ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 19:23:56 +0100 Subject: [PATCH 115/771] Improve eglot-eldoc-function Use the :range key if the server provided it. Also simplify code with a new eglot--with-lsp-range macro. * eglot.el (eglot--format-markup): Tweak comment. (eglot--with-lsp-range): New helper macro. (eglot--server-textDocument/publishDiagnostics) (eglot--apply-text-edits): Use it. (eglot-eldoc-function): Use range if server provides it. --- lisp/progmodes/eglot.el | 69 +++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d1e4d1548a4..b2709a6ae47 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -740,7 +740,7 @@ DEFERRED is passed to `eglot--request', which see." "Format MARKUP according to LSP's spec." (cond ((stringp markup) (with-temp-buffer - (ignore-errors (funcall (intern "markdown-mode"))) ;escape bytecompiler + (ignore-errors (funcall (intern "markdown-mode"))) ;escape bytecomp (font-lock-ensure) (insert markup) (string-trim (buffer-string)))) @@ -757,6 +757,15 @@ DEFERRED is passed to `eglot--request', which see." "Determine if current server is capable of FEAT." (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) +(cl-defmacro eglot--with-lsp-range ((start end) range &body body + &aux (range-sym (cl-gensym))) + "Bind LSP RANGE to START and END. Evaluate BODY." + (declare (indent 2) (debug (sexp sexp &rest form))) + `(let* ((,range-sym ,range) + (,start (eglot--lsp-position-to-point (plist-get ,range-sym :start))) + (,end (eglot--lsp-position-to-point (plist-get ,range-sym :end)))) + ,@body)) + ;;; Minor modes ;;; @@ -965,17 +974,13 @@ called interactively." collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (cl-destructuring-bind (&key start end) - range - (let* ((begin-pos (eglot--lsp-position-to-point start)) - (end-pos (eglot--lsp-position-to-point end))) - (flymake-make-diagnostic - (current-buffer) - begin-pos end-pos - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message))))) + (eglot--with-lsp-range (beg end) range + (flymake-make-diagnostic (current-buffer) + beg end + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message)))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1302,15 +1307,23 @@ DUMMY is ignored" (proc (eglot--current-process-or-lose)) (position-params (eglot--current-buffer-TextDocumentPositionParams))) (when (eglot--server-capable :hoverProvider) - (eglot--request proc :textDocument/hover position-params - :success-fn (eglot--lambda (&key contents _range) - (eldoc-message - (mapconcat #'eglot--format-markup - (if (vectorp contents) - contents - (list contents)) - "\n"))) - :deferred :textDocument/hover)) + (eglot--request + proc :textDocument/hover position-params + :success-fn (eglot--lambda (&key contents range) + (when (get-buffer-window buffer) + (with-current-buffer buffer + (eldoc-message + (concat + (and range + (eglot--with-lsp-range (beg end) range + (concat (buffer-substring beg end) ": "))) + (mapconcat #'eglot--format-markup + (append + (cond ((vectorp contents) + contents) + (contents + (list contents)))) "\n")))))) + :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--request proc :textDocument/documentHighlight position-params @@ -1321,11 +1334,8 @@ DUMMY is ignored" (with-current-buffer buffer (eglot--mapply (eglot--lambda (&key range kind) - (cl-destructuring-bind (&key start end) range - (let ((ov (make-overlay - (eglot--lsp-position-to-point start) - (eglot--lsp-position-to-point end) - buffer))) + (eglot--with-lsp-range (beg end) range + (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) (overlay-put ov 'evaporate t) (overlay-put ov :kind kind) @@ -1366,11 +1376,8 @@ DUMMY is ignored" (save-restriction (widen) (save-excursion - (let ((start (eglot--lsp-position-to-point (plist-get range :start)))) - (goto-char start) - (delete-region start - (eglot--lsp-position-to-point (plist-get range :end))) - (insert newText))))) + (eglot--with-lsp-range (beg end) range + (goto-char beg) (delete-region beg end) (insert newText))))) edits) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits)))) From e36892ef513e753bbef6787d15d56b6e669dbcfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 20:00:15 +0100 Subject: [PATCH 116/771] Friendlier m-x eglot * eglot.el (eglot-server-programs): Renamed from eglot-executables (eglot--interactive): Redesign (eglot): Docstring. (eglot--connect): Now a synchronous gig. (eglot--interactive): Friendlier. (eglot): Improve docstring, rework a bit. (eglot-reconnect): Rework a bit. (eglot--process-sentinel): Insert "byebye" ruler here. * README.md: Update --- lisp/progmodes/eglot.el | 140 ++++++++++++++++++++-------------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b2709a6ae47..22799cc299d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -47,9 +47,10 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-executables '((rust-mode . ("rls")) - (python-mode . ("pyls")) - (js-mode . ("javascript-typescript-stdio"))) +(defvar eglot-server-programs '((rust-mode . ("rls")) + (python-mode . ("pyls")) + (js-mode . ("javascript-typescript-stdio")) + (sh-mode . ("bash-language-server" "start"))) "Alist mapping major modes to server executables.") (defface eglot-mode-line @@ -204,10 +205,8 @@ CONTACT is as `eglot--contact'. Returns a process object." :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) -(defun eglot--connect (project managed-major-mode - short-name contact &optional success-fn) - "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. -SUCCESS-FN with no args if all goes well." +(defun eglot--connect (project managed-major-mode short-name contact) + "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT." (let* ((proc (eglot--make-process short-name managed-major-mode contact)) (buffer (process-buffer proc))) (setf (eglot--contact proc) contact @@ -216,70 +215,68 @@ SUCCESS-FN with no args if all goes well." (with-current-buffer buffer (let ((inhibit-read-only t)) (setf (eglot--short-name proc) short-name) - (push proc - (gethash (project-current) - eglot--processes-by-project)) + (push proc (gethash project eglot--processes-by-project)) (erase-buffer) (read-only-mode t) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert - (format "\n-----------------------------------\n")))) - (eglot--request - proc - :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :rootUri (eglot--path-to-uri - (car (project-roots (project-current)))) - :initializationOptions [] - :capabilities (eglot--client-capabilities)) - :success-fn - (cl-function - (lambda (&key capabilities) - (setf (eglot--capabilities proc) capabilities) - (setf (eglot--status proc) nil) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))) - (when success-fn (funcall success-fn proc)) - (eglot--notify proc :initialized (eglot--obj :__dummy__ t))))))))) + (cl-destructuring-bind (&key capabilities) + (eglot--sync-request + proc + :initialize + (eglot--obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [] + :capabilities (eglot--client-capabilities))) + (setf (eglot--capabilities proc) capabilities) + (setf (eglot--status proc) nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc))) + (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) + proc))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") (defun eglot--interactive () "Helper for `eglot'." - (let* ((managed-major-mode + (let* ((guessed-mode (if buffer-file-name major-mode)) + (managed-mode (cond - ((or current-prefix-arg - (not buffer-file-name)) + ((or (>= (prefix-numeric-value current-prefix-arg) 16) + (not guessed-mode)) (intern (completing-read "[eglot] Start a server to manage buffers of what major mode? " (mapcar #'symbol-name (eglot--all-major-modes)) nil t - (symbol-name major-mode) nil - (symbol-name major-mode) nil))) - (t major-mode))) - (guessed-command - (cdr (assoc managed-major-mode eglot-executables)))) + (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) + (t guessed-mode))) + (guessed-command (cdr (assoc managed-mode eglot-server-programs))) + (base-prompt "[eglot] Enter program to execute (or :): ") + (prompt + (cond (current-prefix-arg base-prompt) + ((null guessed-command) + (concat (format "[eglot] Sorry, couldn't guess for `%s'!" + managed-mode) + "\n" base-prompt)) + ((and (listp guessed-command) + (not (executable-find (car guessed-command)))) + (concat (format "[eglot] I guess you want to run `%s'" + (combine-and-quote-strings guessed-command)) + (format ", but I can't find `%s' in PATH!" + (car guessed-command)) + "\n" base-prompt))))) (list - managed-major-mode - (let ((prompt - (cond (current-prefix-arg - "[eglot] Enter program to execute (or :): ") - ((null guessed-command) - (format "[eglot] Sorry, couldn't guess for `%s'!\n\ -Enter program to execute (or :): " - managed-major-mode))))) - (if prompt - (split-string-and-unquote - (read-shell-command prompt - (if (listp guessed-command) - (combine-and-quote-strings guessed-command)) - 'eglot-command-history)) - guessed-command)) + managed-mode + (if prompt + (split-string-and-unquote + (read-shell-command prompt + (if (listp guessed-command) + (combine-and-quote-strings guessed-command)) + 'eglot-command-history)) + guessed-command) t))) ;;;###autoload @@ -296,8 +293,11 @@ is also know as the server's \"contact\". MANAGED-MAJOR-MODE is an Emacs major mode. -With a prefix arg, prompt for MANAGED-MAJOR-MODE and COMMAND, -else guess them from current context and `eglot-executables'. +Interactively, guess MANAGED-MAJOR-MODE from current buffer and +COMMAND from `eglot-server-programs'. With a single +\\[universal-argument] prefix arg, prompt for COMMAND. With two +\\[universal-argument] prefix args, also prompt for +MANAGED-MAJOR-MODE. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) @@ -313,16 +313,13 @@ INTERACTIVE is t if called interactively." (eglot-reconnect current-process interactive) (when (process-live-p current-process) (eglot-shutdown current-process)) - (eglot--connect project - managed-major-mode - short-name - command - (lambda (proc) - (eglot--message "Connected! Process `%s' now \ + (let ((proc (eglot--connect project + managed-major-mode + short-name + command))) + (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." - proc - managed-major-mode - short-name))))))) + proc managed-major-mode short-name)))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. @@ -333,8 +330,8 @@ INTERACTIVE is t if called interactively." (eglot--connect (eglot--project process) (eglot--major-mode process) (eglot--short-name process) - (eglot--contact process) - (lambda (_proc) (eglot--message "Reconnected!")))) + (eglot--contact process)) + (eglot--message "Reconnected!")) (defvar eglot--inhibit-auto-reconnect nil "If non-nil, don't autoreconnect on unexpected quit.") @@ -343,6 +340,9 @@ INTERACTIVE is t if called interactively." "Called when PROC undergoes CHANGE." (eglot--log-event proc `(:message "Process state changed" :change ,change)) (when (not (process-live-p proc)) + (with-current-buffer (eglot-events-buffer proc) + (let ((inhibit-read-only t)) + (insert "\n----------b---y---e---b---y---e----------\n"))) ;; Cancel outstanding timers (maphash (lambda (_id triplet) (cl-destructuring-bind (_success _error timeout) triplet From 0f73b0ef434329b59f9db5065a239afe49be2fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 21:31:37 +0100 Subject: [PATCH 117/771] Rename functions. eglot--request is now the synchronous one * eglot.el (eglot--connect, eglot-shutdown) (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point, eglot-rename): Call eglot--request. (eglot--async-request): Renamed from eglot--request. (eglot--request): Renamed from eglot--sync-request. (eglot--TextDocumentIdentifier) (eglot--VersionedTextDocumentIdentifier) (eglot--TextDocumentPositionParams, eglot--TextDocumentItem): Renamed from the more verbose eglot--current-buffer-* variante. (eglot-rename, eglot-imenu, eglot-eldoc-function) (eglot-completion-at-point, xref-backend-definitions) (xref-backend-identifier-at-point) (eglot--signal-textDocument/didSave) (xref-backend-identifier-completion-table) (eglot--signal-textDocument/didClose) (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didChange): Use new function names. --- lisp/progmodes/eglot.el | 140 +++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 72 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22799cc299d..58a2374bbfe 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -219,7 +219,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (erase-buffer) (read-only-mode t) (cl-destructuring-bind (&key capabilities) - (eglot--sync-request + (eglot--request proc :initialize (eglot--obj :processId (unless (eq (process-type proc) @@ -558,9 +558,9 @@ is a symbol saying if this is a client or server originated." (defvar eglot--ready-predicates '(eglot--server-ready-p) "Special hook of predicates controlling deferred actions. -When one of these functions returns nil, a deferrable -`eglot--request' will be deferred. Each predicate is passed the -an symbol for the request request and a process object.") +If one of these returns nil, a deferrable `eglot--async-request' +will be deferred. Each predicate is passed the symbol for the +request request and a process object.") (defun eglot--server-ready-p (_what _proc) "Tell if server of PROC ready for processing deferred WHAT." @@ -570,13 +570,13 @@ an symbol for the request request and a process object.") (declare (indent 1) (debug (sexp &rest form))) `(cl-function (lambda ,cl-lambda-list ,@body))) -(cl-defun eglot--request (proc - method - params - &rest args - &key success-fn error-fn timeout-fn - (timeout eglot-request-timeout) - (deferred nil)) +(cl-defun eglot--async-request (proc + method + params + &rest args + &key success-fn error-fn timeout-fn + (timeout eglot-request-timeout) + (deferred nil)) "Make a request to PROCESS, expecting a reply. Return the ID of this request. Wait TIMEOUT seconds for response. If DEFERRED, maybe defer request to the future, or never at all, @@ -610,11 +610,11 @@ timeout keeps counting." (when (buffer-live-p buf) (with-current-buffer buf (save-excursion (goto-char point) - (apply #'eglot--request proc + (apply #'eglot--async-request proc method params args))))))) (puthash (list deferred buf) (list later (funcall make-timeout)) (eglot--deferred-actions proc)) - (cl-return-from eglot--request nil))))) + (cl-return-from eglot--async-request nil))))) ;; Really run it ;; (puthash id @@ -634,17 +634,17 @@ timeout keeps counting." :method method :params params)))) -(defun eglot--sync-request (proc method params &optional deferred) - "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. +(defun eglot--request (proc method params &optional deferred) + "Like `eglot--async-request' for PROC, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally. -DEFERRED is passed to `eglot--request', which see." +DEFERRED is passed to `eglot--async-request', which see." ;; Launching a deferred sync request with outstanding changes is a ;; bad idea, since that might lead to the request never having a ;; chance to run, because `eglot--ready-predicates'. (when deferred (eglot--signal-textDocument/didChange)) - (let* ((done (make-symbol "eglot--sync-request-catch-tag")) + (let* ((done (make-symbol "eglot--request-catch-tag")) (res - (catch done (eglot--request + (catch done (eglot--async-request proc method params :success-fn (lambda (&rest args) (throw done (if (vectorp (car args)) @@ -914,11 +914,11 @@ called interactively." (unwind-protect (let ((eglot-request-timeout 3)) (setf (eglot--moribund proc) t) - (eglot--sync-request proc - :shutdown - nil) + (eglot--request proc + :shutdown + nil) ;; this one should always fail - (ignore-errors (eglot--sync-request proc :exit nil))) + (ignore-errors (eglot--request proc :exit nil))) (when (process-live-p proc) (eglot--warn "Brutally deleting existing process %s" proc) (delete-process proc)))) @@ -1031,21 +1031,21 @@ called interactively." (eglot--obj :code -32001 :message (format "%s" err)))))) -(defun eglot--current-buffer-TextDocumentIdentifier () +(defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) (defvar-local eglot--versioned-identifier 0) -(defun eglot--current-buffer-VersionedTextDocumentIdentifier () +(defun eglot--VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." - (append (eglot--current-buffer-TextDocumentIdentifier) + (append (eglot--TextDocumentIdentifier) (eglot--obj :version eglot--versioned-identifier))) -(defun eglot--current-buffer-TextDocumentItem () +(defun eglot--TextDocumentItem () "Compute TextDocumentItem object for current buffer." (append - (eglot--current-buffer-VersionedTextDocumentIdentifier) + (eglot--VersionedTextDocumentIdentifier) (eglot--obj :languageId (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) (match-string 1 (symbol-name major-mode)) @@ -1055,9 +1055,9 @@ called interactively." (widen) (buffer-substring-no-properties (point-min) (point-max)))))) -(defun eglot--current-buffer-TextDocumentPositionParams () +(defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." - (eglot--obj :textDocument (eglot--current-buffer-TextDocumentIdentifier) + (eglot--obj :textDocument (eglot--TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil @@ -1104,7 +1104,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." proc :textDocument/didChange (eglot--obj :textDocument - (eglot--current-buffer-VersionedTextDocumentIdentifier) + (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector (eglot--obj @@ -1126,14 +1126,14 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--notify (eglot--current-process-or-lose) :textDocument/didOpen (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentItem)))) + (eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." (eglot--notify (eglot--current-process-or-lose) :textDocument/didClose (eglot--obj :textDocument - (eglot--current-buffer-TextDocumentIdentifier)))) + (eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." @@ -1142,7 +1142,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." :textDocument/willSave (eglot--obj :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha - :textDocument (eglot--current-buffer-TextDocumentIdentifier)))) + :textDocument (eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." @@ -1152,7 +1152,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) - :textDocument (eglot--current-buffer-TextDocumentIdentifier)))) + :textDocument (eglot--TextDocumentIdentifier)))) (defun eglot-flymake-backend (report-fn &rest _more) "An EGLOT Flymake backend. @@ -1190,7 +1190,7 @@ DUMMY is ignored" (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) (let ((proc (eglot--current-process-or-lose)) - (text-id (eglot--current-buffer-TextDocumentIdentifier))) + (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols @@ -1205,17 +1205,17 @@ DUMMY is ignored" :locations (list location) :kind kind :containerName containerName)) - (eglot--sync-request proc - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) + (eglot--request proc + :textDocument/documentSymbol + (eglot--obj + :textDocument text-id)))) (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) (when-let ((symatpt (symbol-at-point))) (propertize (symbol-name symatpt) :textDocumentPositionParams - (eglot--current-buffer-TextDocumentPositionParams)))) + (eglot--TextDocumentPositionParams)))) (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) (let* ((rich-identifier @@ -1223,10 +1223,10 @@ DUMMY is ignored" (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (eglot--sync-request (eglot--current-process-or-lose) - :textDocument/definition - (get-text-property - 0 :textDocumentPositionParams identifier))))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/definition + (get-text-property + 0 :textDocumentPositionParams identifier))))) (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) @@ -1244,12 +1244,12 @@ DUMMY is ignored" (eglot--mapply (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (eglot--sync-request (eglot--current-process-or-lose) - :textDocument/references - (append - params - (eglot--obj :context - (eglot--obj :includeDeclaration t))))))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/references + (append + params + (eglot--obj :context + (eglot--obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) @@ -1258,9 +1258,9 @@ DUMMY is ignored" (let ((range (plist-get location :range)) (uri (plist-get location :uri))) (eglot--xref-make name uri (plist-get range :start)))) - (eglot--sync-request (eglot--current-process-or-lose) - :workspace/symbol - (eglot--obj :query pattern))))) + (eglot--request (eglot--current-process-or-lose) + :workspace/symbol + (eglot--obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." @@ -1272,11 +1272,10 @@ DUMMY is ignored" (or (cdr bounds) (point)) (completion-table-with-cache (lambda (_ignored) - (let* ((resp (eglot--sync-request - proc - :textDocument/completion - (eglot--current-buffer-TextDocumentPositionParams) - :textDocument/completion)) + (let* ((resp (eglot--request proc + :textDocument/completion + (eglot--TextDocumentPositionParams) + :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail @@ -1305,9 +1304,9 @@ DUMMY is ignored" "EGLOT's `eldoc-documentation-function' function." (let ((buffer (current-buffer)) (proc (eglot--current-process-or-lose)) - (position-params (eglot--current-buffer-TextDocumentPositionParams))) + (position-params (eglot--TextDocumentPositionParams))) (when (eglot--server-capable :hoverProvider) - (eglot--request + (eglot--async-request proc :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) (when (get-buffer-window buffer) @@ -1325,7 +1324,7 @@ DUMMY is ignored" (list contents)))) "\n")))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) - (eglot--request + (eglot--async-request proc :textDocument/documentHighlight position-params :success-fn (lambda (highlights) (mapc #'delete-overlay eglot--highlights) @@ -1353,11 +1352,10 @@ DUMMY is ignored" (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (eglot--sync-request - (eglot--current-process-or-lose) - :textDocument/documentSymbol - (eglot--obj - :textDocument (eglot--current-buffer-TextDocumentIdentifier)))))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/documentSymbol + (eglot--obj + :textDocument (eglot--TextDocumentIdentifier)))))) (append (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries) @@ -1425,11 +1423,9 @@ Proceed? " (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (eglot--sync-request (eglot--current-process-or-lose) - :textDocument/rename - (append - (eglot--current-buffer-TextDocumentPositionParams) - (eglot--obj :newName newname))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/rename `(,@(eglot--TextDocumentPositionParams) + ,@(eglot--obj :newName newname))) current-prefix-arg)) @@ -1448,7 +1444,7 @@ Proceed? " (defun eglot--rls-probably-ready-for-p (what proc) "Guess if the RLS running in PROC is ready for WHAT." (or (eq what :textDocument/completion) ; RLS normally ready for this - ; one, even if building + ; one, even if building ; (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner proc))) (and (equal "Indexing" what) done)))) From 572bb298b26715d4451cacd6ac91685fb0d2ddec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 21:56:54 +0100 Subject: [PATCH 118/771] Support :completionitem/resolve This is quite handy with company and company-quickhelp * eglot.el (eglot-completion-at-point): Send :completionItem/resolve * README.md: Mention completionItem/resolve --- lisp/progmodes/eglot.el | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58a2374bbfe..35573034bf7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1278,23 +1278,35 @@ DUMMY is ignored" :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply - (eglot--lambda (&key insertText label kind detail - documentation sortText &allow-other-keys) - (propertize (or insertText label) - :kind-name (cdr (assoc kind eglot--kind-names)) - :detail detail - :documentation documentation :sortText sortText)) + (eglot--lambda (&rest all &key label &allow-other-keys) + (add-text-properties 0 1 all label) label) items)))) :annotation-function - (lambda (what) - (propertize (concat " " (or (get-text-property 0 :detail what) - (get-text-property 0 :kind what))) + (lambda (obj) + (propertize (concat " " (or (get-text-property 0 :detail obj) + (cdr (assoc (get-text-property 0 :kind obj) + eglot--kind-names)))) 'face 'font-lock-function-name-face)) :display-sort-function - (lambda (items) (sort items (lambda (a b) - (string-lessp - (get-text-property 0 :sortText a) - (get-text-property 0 :sortText b))))) + (lambda (items) + (sort items (lambda (a b) + (string-lessp + (or (get-text-property 0 :sortText a) "") + (or (get-text-property 0 :sortText b) ""))))) + :company-doc-buffer + (lambda (obj) + (let ((documentation + (or (get-text-property 0 :documentation obj) + (plist-get (eglot--request proc :completionItem/resolve + (text-properties-at 0 obj)) + :documentation)))) + (when documentation + (with-current-buffer (get-buffer-create " *eglot doc*") + (erase-buffer) + (ignore-errors (funcall (intern "markdown-mode"))) + (font-lock-ensure) + (insert documentation) + (current-buffer))))) :exit-function (lambda (_string _status) (eglot-eldoc-function)))))) From 3d193f2f33bb5524fd840f9f82ecc5adeabffa01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 22:04:59 +0100 Subject: [PATCH 119/771] Misc little adjustments for readability * eglot.el (eglot--log-event, eglot--process-receive) (eglot--xref-make, xref-backend-apropos): Use cl-destructuring-bind. (eglot--server-window/showMessageRequest): Compact. --- lisp/progmodes/eglot.el | 122 +++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 64 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 35573034bf7..4c5d53e3204 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -467,62 +467,58 @@ INTERACTIVE is t if called interactively." PROC is the current process. MESSAGE is a JSON-like plist. TYPE is a symbol saying if this is a client or server originated." (with-current-buffer (eglot-events-buffer proc) - (let* ((inhibit-read-only t) - (id (plist-get message :id)) - (error (plist-get message :error)) - (method (plist-get message :method)) - (subtype (cond ((and method id) 'request) - (method 'notification) - (id 'reply) - (t 'message))) - (type - (format "%s-%s" (or type :internal) subtype))) - (goto-char (point-max)) - (let ((msg (format "%s%s%s:\n%s\n" - type - (if id (format " (id:%s)" id) "") - (if error " ERROR" "") - (pp-to-string message)))) - (when error - (setq msg (propertize msg 'face 'error))) - (insert-before-markers msg))))) + (cl-destructuring-bind (&key method id error &allow-other-keys) message + (let* ((inhibit-read-only t) + (subtype (cond ((and method id) 'request) + (method 'notification) + (id 'reply) + (t 'message))) + (type + (format "%s-%s" (or type :internal) subtype))) + (goto-char (point-max)) + (let ((msg (format "%s%s%s:\n%s\n" + type + (if id (format " (id:%s)" id) "") + (if error " ERROR" "") + (pp-to-string message)))) + (when error + (setq msg (propertize msg 'face 'error))) + (insert-before-markers msg)))))) (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." - (let* ((id (plist-get message :id)) - (method (plist-get message :method)) - (err (plist-get message :error)) - (continuations (and id - (not method) - (gethash id (eglot--pending-continuations proc))))) - (eglot--log-event proc message 'server) - (when err (setf (eglot--status proc) `(,err t))) - (cond (method - ;; a server notification or a server request - (let* ((handler-sym (intern (concat "eglot--server-" method)))) - (if (functionp handler-sym) - (apply handler-sym proc (append - (plist-get message :params) - (if id `(:id ,id)))) - (eglot--warn "No implementation of method %s yet" method) - (when id - (eglot--reply - proc id - :error (eglot--obj :code -32601 - :message "Method unimplemented")))))) - (continuations - (cancel-timer (cl-third continuations)) - (remhash id (eglot--pending-continuations proc)) - (if err - (apply (cl-second continuations) err) - (let ((res (plist-get message :result))) - (if (listp res) - (apply (cl-first continuations) res) - (funcall (cl-first continuations) res))))) - (id - (eglot--warn "Ooops no continuation for id %s" id))) - (eglot--call-deferred proc) - (force-mode-line-update t))) + (cl-destructuring-bind (&key method id error &allow-other-keys) message + (let* ((continuations (and id + (not method) + (gethash id (eglot--pending-continuations proc))))) + (eglot--log-event proc message 'server) + (when error (setf (eglot--status proc) `(,error t))) + (cond (method + ;; a server notification or a server request + (let* ((handler-sym (intern (concat "eglot--server-" method)))) + (if (functionp handler-sym) + (apply handler-sym proc (append + (plist-get message :params) + (if id `(:id ,id)))) + (eglot--warn "No implementation of method %s yet" method) + (when id + (eglot--reply + proc id + :error (eglot--obj :code -32601 + :message "Method unimplemented")))))) + (continuations + (cancel-timer (cl-third continuations)) + (remhash id (eglot--pending-continuations proc)) + (if error + (apply (cl-second continuations) error) + (let ((res (plist-get message :result))) + (if (listp res) + (apply (cl-first continuations) res) + (funcall (cl-first continuations) res))))) + (id + (eglot--warn "Ooops no continuation for id %s" id))) + (eglot--call-deferred proc) + (force-mode-line-update t)))) (defvar eglot--expect-carriage-return nil) @@ -941,10 +937,9 @@ called interactively." 'face (if (<= type 1) 'error)) type message) "\nChoose an option: ") - (mapcar (lambda (obj) (plist-get obj :title)) actions) - nil - t - (plist-get (elt actions 0) :title))) + (or (mapcar (lambda (obj) (plist-get obj :title)) actions) + '("OK")) + nil t (plist-get (elt actions 0) :title))) (if reply (eglot--reply process id :result (eglot--obj :title reply)) (eglot--reply process id @@ -1181,11 +1176,11 @@ DUMMY is ignored" (defun eglot--xref-make (name uri position) "Like `xref-make' but with LSP's NAME, URI and POSITION." - (xref-make name (xref-make-file-location - (eglot--uri-to-path uri) - ;; F!@(#*&#$)CKING OFF-BY-ONE again - (1+ (plist-get position :line)) - (plist-get position :character)))) + (cl-destructuring-bind (line character) position + (xref-make name (xref-make-file-location + (eglot--uri-to-path uri) + ;; F!@(#*&#$)CKING OFF-BY-ONE again + (1+ line) character)))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) @@ -1255,8 +1250,7 @@ DUMMY is ignored" (when (eglot--server-capable :workspaceSymbolProvider) (eglot--mapply (eglot--lambda (&key name location &allow-other-keys) - (let ((range (plist-get location :range)) - (uri (plist-get location :uri))) + (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) (eglot--request (eglot--current-process-or-lose) :workspace/symbol From dfd5947b11edf20b9c95a272d195b8fd737a855c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 22:56:11 +0100 Subject: [PATCH 120/771] (eglot--xref-make): fix use of cl-destructuring-bind. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4c5d53e3204..635ca26632e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1176,7 +1176,7 @@ DUMMY is ignored" (defun eglot--xref-make (name uri position) "Like `xref-make' but with LSP's NAME, URI and POSITION." - (cl-destructuring-bind (line character) position + (cl-destructuring-bind (&key line character) position (xref-make name (xref-make-file-location (eglot--uri-to-path uri) ;; F!@(#*&#$)CKING OFF-BY-ONE again From abfe41cc2c2932525844d9d1cef83bf877145bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 10 May 2018 22:40:32 +0100 Subject: [PATCH 121/771] Prepare to sumbit to gnu elpa * eglot.el: Update headers. --- lisp/progmodes/eglot.el | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 635ca26632e..e33e54045c5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,12 +1,13 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2017 João Távora +;; Copyright (C) 2003-2018 Free Software Foundation, Inc. ;; Version: 0.1 -;; Author: João Távora -;; Url: https://github.com/joaotavora/eglot +;; Author: João Távora +;; Maintainer: João Távora +;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1")) +;; Package-Requires: ((emacs "26.1") (json-mode "1.6.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 From 1251bd1336077b9a3490929fd2aa2c70acada495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 11 May 2018 02:03:10 +0100 Subject: [PATCH 122/771] Duh, json.el is in emacs, and json-mode.el is useless here * eglot.el (Package-Requires): Don't require json-mode --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e33e54045c5..3b3aada055b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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") (json-mode "1.6.0")) +;; Package-Requires: ((emacs "26.1")) ;; 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 From 4af0193fad1ca40b48db4fba5b27e8a6f6c8d11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 11 May 2018 10:59:59 +0100 Subject: [PATCH 123/771] Rework autoreconnection logic Can't be a global var, has to be a per process thing. * eglot.el (eglot-autoreconnect): New defcustom (eglot--inhibit-autoreconnect): Renamed from eglot--inhibit-autoreconnect (eglot--connect): Run autoreconnect timer here. (eglot--inhibit-auto-reconnect): Removed. (eglot--process-sentinel): Don't run timer here. Rework. (eglot, eglot-reconnect): Pass INTERACTIVE to eglot--connect. --- lisp/progmodes/eglot.el | 57 +++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3b3aada055b..aabaf5407f5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -59,9 +59,19 @@ "Face for package-name in EGLOT's mode line.") (defcustom eglot-request-timeout 10 - "How many seconds to way for a reply from the server." + "How many seconds to wait for a reply from the server." :type :integer) +(defcustom eglot-autoreconnect 3 + "Control EGLOT's ability to reconnect automatically. +If t, always reconnect automatically (not recommended). If nil, +never reconnect automatically after unexpected server shutdowns, +crashes or network failures. A positive integer number says to +only autoreconnect if the previous successful connection attempt +lasted more than that many seconds." + :type '(choice (boolean :tag "Whether to inhibit autoreconnection") + (integer :tag "Number of seconds"))) + ;;; Process management (defvar eglot--processes-by-project (make-hash-table :test #'equal) @@ -129,6 +139,9 @@ A list (ID WHAT DONE-P).") "Status as declared by the server. A list (WHAT SERIOUS-P).") +(eglot--define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect + "If non-nil, don't autoreconnect on unexpected quit.") + (eglot--define-process-var eglot--contact nil "Method used to contact a server. Either a list of strings (a shell command and arguments), or a @@ -206,8 +219,9 @@ CONTACT is as `eglot--contact'. Returns a process object." :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) -(defun eglot--connect (project managed-major-mode short-name contact) - "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT." +(defun eglot--connect (project managed-major-mode short-name contact interactive) + "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. +INTERACTIVE is t if inside interactive call." (let* ((proc (eglot--make-process short-name managed-major-mode contact)) (buffer (process-buffer proc))) (setf (eglot--contact proc) contact @@ -215,6 +229,15 @@ CONTACT is as `eglot--contact'. Returns a process object." (eglot--major-mode proc) managed-major-mode) (with-current-buffer buffer (let ((inhibit-read-only t)) + (setf (eglot--inhibit-autoreconnect proc) + (cond + ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) + (interactive nil) + ((cl-plusp eglot-autoreconnect) + (run-with-timer eglot-autoreconnect nil + (lambda () + (setf (eglot--inhibit-autoreconnect proc) + (null eglot-autoreconnect))))))) (setf (eglot--short-name proc) short-name) (push proc (gethash project eglot--processes-by-project)) (erase-buffer) @@ -317,7 +340,8 @@ INTERACTIVE is t if called interactively." (let ((proc (eglot--connect project managed-major-mode short-name - command))) + command + interactive))) (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." proc managed-major-mode short-name)))))) @@ -331,12 +355,10 @@ INTERACTIVE is t if called interactively." (eglot--connect (eglot--project process) (eglot--major-mode process) (eglot--short-name process) - (eglot--contact process)) + (eglot--contact process) + interactive) (eglot--message "Reconnected!")) -(defvar eglot--inhibit-auto-reconnect nil - "If non-nil, don't autoreconnect on unexpected quit.") - (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." (eglot--log-event proc `(:message "Process state changed" :change ,change)) @@ -364,22 +386,13 @@ INTERACTIVE is t if called interactively." (setf (gethash (eglot--project proc) eglot--processes-by-project) (delq proc (gethash (eglot--project proc) eglot--processes-by-project))) - (cond ((eglot--moribund proc) - (eglot--message "(sentinel) Moribund process exited with status %s" - (process-exit-status proc))) - ((null eglot--inhibit-auto-reconnect) - (eglot--warn - "(sentinel) Reconnecting after process unexpectedly changed to `%s'." - change) - (setq eglot--inhibit-auto-reconnect - (run-with-timer 3 nil - (lambda () - (setq eglot--inhibit-auto-reconnect nil)))) + (eglot--message "Server exited with status %s" (process-exit-status proc)) + (cond ((eglot--moribund proc)) + ((not (eglot--inhibit-autoreconnect proc)) + (eglot--warn "Reconnecting unexpected server exit.") (eglot-reconnect proc)) (t - (eglot--warn - "(sentinel) Not auto-reconnecting, last one didn't last long." - change))) + (eglot--warn "Not auto-reconnecting, last one didn't last long."))) (delete-process proc)))) (defun eglot--process-filter (proc string) From fabee14ed5b32c30c6ac5cb1ce88f387522a6a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 11 May 2018 11:46:53 +0100 Subject: [PATCH 124/771] Get rid of catch/loop/throw idiom (suggested by thien-thi nguyen) * eglot.el (eglot--process-filter) (eglot--request): Replace catch/loop/throw idiom with let/test/loop/set --- lisp/progmodes/eglot.el | 44 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aabaf5407f5..c8612cf1404 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -400,8 +400,7 @@ INTERACTIVE is t if called interactively." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) (let ((inhibit-read-only t) - (expected-bytes (eglot--expected-bytes proc)) - (done (make-symbol "eglot--process-filter-done-tag"))) + (expected-bytes (eglot--expected-bytes proc))) ;; Insert the text, advancing the process marker. ;; (save-excursion @@ -411,8 +410,8 @@ INTERACTIVE is t if called interactively." ;; Loop (more than one message might have arrived) ;; (unwind-protect - (catch done - (while t + (let (done) + (while (not done) (cond ((not expected-bytes) ;; Starting a new message @@ -425,7 +424,7 @@ INTERACTIVE is t if called interactively." t) (string-to-number (match-string 1)))) (unless expected-bytes - (throw done :waiting-for-new-message))) + (setq done :waiting-for-new-message))) (t ;; Attempt to complete a message body ;; @@ -453,7 +452,7 @@ INTERACTIVE is t if called interactively." (t ;; Message is still incomplete ;; - (throw done :waiting-for-more-bytes-in-this-message)))))))) + (setq done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; (setf (eglot--expected-bytes proc) expected-bytes)))))) @@ -652,25 +651,20 @@ DEFERRED is passed to `eglot--async-request', which see." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `eglot--ready-predicates'. (when deferred (eglot--signal-textDocument/didChange)) - (let* ((done (make-symbol "eglot--request-catch-tag")) - (res - (catch done (eglot--async-request - proc method params - :success-fn (lambda (&rest args) - (throw done (if (vectorp (car args)) - (car args) args))) - :error-fn (eglot--lambda - (&key code message &allow-other-keys) - (throw done - `(error ,(format "Oops: %s: %s" - code message)))) - :timeout-fn (lambda () - (throw done '(error "Timed out"))) - :deferred deferred) - ;; now spin, baby! - (while t (accept-process-output nil 0.01))))) - (when (and (listp res) (eq 'error (car res))) (eglot--error (cadr res))) - res)) + (let ((retval)) + (eglot--async-request + proc method params + :success-fn (lambda (&rest args) + (setq retval `(done ,(if (vectorp (car args)) + (car args) args)))) + :error-fn (eglot--lambda (&key code message &allow-other-keys) + (setq retval `(error ,(format "Oops: %s: %s" code message)))) + :timeout-fn (lambda () + (setq retval '(error "Timed out"))) + :deferred deferred) + (while (not retval) (accept-process-output nil 30)) + (when (eq 'error (car retval)) (eglot--error (cadr retval))) + (cadr retval))) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" From 72b7487c55bfc694eec23391e116a6866c40c949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 12 May 2018 15:59:53 +0100 Subject: [PATCH 125/771] New command eglot-help-at-point and a readme update * README.md (Commands and keybindings): New section. * eglot.el (eglot-eldoc-function): Use eglot--hover-info. Don't care about kind in highlightSymbol (eglot--hover-info): New helper. (eglot-help-at-point): New command. --- lisp/progmodes/eglot.el | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c8612cf1404..13f6f61d581 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1314,6 +1314,28 @@ DUMMY is ignored" (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") +(defun eglot--hover-info (contents &optional range) + (concat (and range + (eglot--with-lsp-range (beg end) range + (concat (buffer-substring beg end) ": "))) + (mapconcat #'eglot--format-markup + (append + (cond ((vectorp contents) + contents) + (contents + (list contents)))) "\n"))) + +(defun eglot-help-at-point () + "Request \"hover\" information for the thing at point." + (interactive) + (cl-destructuring-bind (&key contents range) + (eglot--request (eglot--current-process-or-lose) :textDocument/hover + (eglot--TextDocumentPositionParams)) + (when (seq-empty-p contents) (eglot--error "No hover info here")) + (with-help-window "*eglot help*" + (with-current-buffer standard-output + (insert (eglot--hover-info contents range)))))) + (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." (let ((buffer (current-buffer)) @@ -1325,17 +1347,7 @@ DUMMY is ignored" :success-fn (eglot--lambda (&key contents range) (when (get-buffer-window buffer) (with-current-buffer buffer - (eldoc-message - (concat - (and range - (eglot--with-lsp-range (beg end) range - (concat (buffer-substring beg end) ": "))) - (mapconcat #'eglot--format-markup - (append - (cond ((vectorp contents) - contents) - (contents - (list contents)))) "\n")))))) + (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request @@ -1346,12 +1358,11 @@ DUMMY is ignored" (when (get-buffer-window buffer) (with-current-buffer buffer (eglot--mapply - (eglot--lambda (&key range kind) + (eglot--lambda (&key range _kind) (eglot--with-lsp-range (beg end) range (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) (overlay-put ov 'evaporate t) - (overlay-put ov :kind kind) ov))) highlights))))) :deferred :textDocument/documentHighlight))) @@ -1482,3 +1493,7 @@ Proceed? " (provide 'eglot) ;;; eglot.el ends here + +;; Local Variables: +;; checkdoc-force-docstrings-flag: nil +;; End: From 1aa3018c654adb383ac2beaa88df15302459fb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 12 May 2018 22:05:20 +0100 Subject: [PATCH 126/771] Fix copyright header. obviously not since 2003 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13f6f61d581..4cb3bec50ee 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,6 +1,6 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2003-2018 Free Software Foundation, Inc. +;; Copyright (C) 2018 Free Software Foundation, Inc. ;; Version: 0.1 ;; Author: João Távora From bb08431bca7475dffdc3f37a97e4aea468a83641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 13 May 2018 02:39:32 +0100 Subject: [PATCH 127/771] Reinstate the catch/loop/throw idiom in eglot-request This reverts parts of commit fabee14ed5b32c30c6ac5cb1ce88f387522a6a1e. Unfortunately, this may cause problems when calling the error callbacks directly as in the process sentinel. In that particular scenario the accept-process-output won't have return, because no output has is being handled. Consequently, if we're unlucky, we have another 30 seconds to way before the flag is tested and the loop exits. * eglot.el (eglot-request): Use catch/loop/throw again --- lisp/progmodes/eglot.el | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13f6f61d581..6f9034942d5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -651,20 +651,25 @@ DEFERRED is passed to `eglot--async-request', which see." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `eglot--ready-predicates'. (when deferred (eglot--signal-textDocument/didChange)) - (let ((retval)) - (eglot--async-request - proc method params - :success-fn (lambda (&rest args) - (setq retval `(done ,(if (vectorp (car args)) - (car args) args)))) - :error-fn (eglot--lambda (&key code message &allow-other-keys) - (setq retval `(error ,(format "Oops: %s: %s" code message)))) - :timeout-fn (lambda () - (setq retval '(error "Timed out"))) - :deferred deferred) - (while (not retval) (accept-process-output nil 30)) - (when (eq 'error (car retval)) (eglot--error (cadr retval))) - (cadr retval))) + (let* ((done (make-symbol "eglot--request-catch-tag")) + (res + (catch done (eglot--async-request + proc method params + :success-fn (lambda (&rest args) + (throw done (if (vectorp (car args)) + (car args) args))) + :error-fn (eglot--lambda + (&key code message &allow-other-keys) + (throw done + `(error ,(format "Oops: %s: %s" + code message)))) + :timeout-fn (lambda () + (throw done '(error "Timed out"))) + :deferred deferred) + ;; now spin, baby! + (while t (accept-process-output nil 0.01))))) + (when (and (listp res) (eq 'error (car res))) (eglot--error (cadr res))) + res)) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" From 0804d7da83edc68809b001032480eac8a0cde9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 13 May 2018 03:09:03 +0100 Subject: [PATCH 128/771] Refactor json-rpc lib jrpc.el from eglot.el * eglot.el [too many to mention]: Move lower level functions to jrpc.el. Hook onto jrpc's external interfaces. * jrpc.el: New file --- lisp/progmodes/eglot.el | 852 +++++++++++----------------------------- 1 file changed, 233 insertions(+), 619 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4cb3bec50ee..934270c43f1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -40,6 +40,7 @@ (require 'flymake) (require 'xref) (require 'subr-x) +(require 'jrpc) ;;; User tweakable stuff @@ -58,12 +59,8 @@ '((t (:inherit font-lock-constant-face :weight bold))) "Face for package-name in EGLOT's mode line.") -(defcustom eglot-request-timeout 10 - "How many seconds to wait for a reply from the server." - :type :integer) - (defcustom eglot-autoreconnect 3 - "Control EGLOT's ability to reconnect automatically. + "Control ability to reconnect automatically to the LSP server. If t, always reconnect automatically (not recommended). If nil, never reconnect automatically after unexpected server shutdowns, crashes or network failures. A positive integer number says to @@ -77,115 +74,64 @@ lasted more than that many seconds." (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defun eglot--current-process () +(jrpc-define-process-var eglot--major-mode nil + "The major-mode this server is managing.") + +(jrpc-define-process-var eglot--capabilities :unreported + "Holds list of capabilities that server reported") + +(jrpc-define-process-var eglot--project nil + "The project the server belongs to.") + +(jrpc-define-process-var eglot--spinner `(nil nil t) + "\"Spinner\" used by some servers. +A list (ID WHAT DONE-P).") + +(jrpc-define-process-var eglot--moribund nil + "Non-nil if server is about to exit") + +(jrpc-define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect + "If non-nil, don't autoreconnect on unexpected quit.") + +(defun eglot--on-shutdown (proc) + ;; Turn off `eglot--managed-mode' where appropriate. + (setf (gethash (eglot--project proc) eglot--processes-by-project) + (delq proc + (gethash (eglot--project proc) eglot--processes-by-project))) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (eglot--buffer-managed-p proc) + (eglot--managed-mode -1)))) + (cond ((eglot--moribund proc)) + ((not (eglot--inhibit-autoreconnect proc)) + (eglot--warn "Reconnecting unexpected server exit.") + (eglot-reconnect proc)) + (t + (eglot--warn "Not auto-reconnecting, last one didn't last long.")))) + +(defun eglot-shutdown (proc &optional interactive) + "Politely ask the server PROC to quit. +Forcefully quit it if it doesn't respond. Don't leave this +function with the server still running. INTERACTIVE is t if +called interactively." + (interactive (list (jrpc-current-process-or-lose) t)) + (when interactive (eglot--message "Asking %s politely to terminate" proc)) + (unwind-protect + (let ((jrpc-request-timeout 3)) + (setf (eglot--moribund proc) t) + (jrpc-request proc :shutdown nil) + ;; this one should always fail under normal conditions + (ignore-errors (jrpc-request proc :exit nil))) + (when (process-live-p proc) + (eglot--warn "Brutally deleting existing process %s" proc) + (delete-process proc)))) + +(defun eglot--find-current-process () "The current logical EGLOT process." (let* ((cur (project-current)) (processes (and cur (gethash cur eglot--processes-by-project)))) (cl-find major-mode processes :key #'eglot--major-mode))) -(defun eglot--current-process-or-lose () - "Return the current EGLOT process or error." - (or (eglot--current-process) - (eglot--error "No current EGLOT process%s" - (if (project-current) "" " (Also no current project)")))) - -(defmacro eglot--define-process-var - (var-sym initval &optional doc) - "Define VAR-SYM as a generalized process-local variable. -INITVAL is the default value. DOC is the documentation." - (declare (indent 2)) - `(progn - (put ',var-sym 'function-documentation ,doc) - (defun ,var-sym (proc) - (let* ((plist (process-plist proc)) - (probe (plist-member plist ',var-sym))) - (if probe - (cadr probe) - (let ((def ,initval)) - (process-put proc ',var-sym def) - def)))) - (gv-define-setter ,var-sym (to-store process) - `(let ((once ,to-store)) (process-put ,process ',',var-sym once) once)))) - -(eglot--define-process-var eglot--short-name nil - "A short name for the process") - -(eglot--define-process-var eglot--major-mode nil - "The major-mode this server is managing.") - -(eglot--define-process-var eglot--expected-bytes nil - "How many bytes declared by server") - -(eglot--define-process-var eglot--pending-continuations (make-hash-table) - "A hash table of request ID to continuation lambdas") - -(eglot--define-process-var eglot--events-buffer nil - "A buffer pretty-printing the EGLOT RPC events") - -(eglot--define-process-var eglot--capabilities :unreported - "Holds list of capabilities that server reported") - -(eglot--define-process-var eglot--moribund nil - "Non-nil if server is about to exit") - -(eglot--define-process-var eglot--project nil - "The project the server belongs to.") - -(eglot--define-process-var eglot--spinner `(nil nil t) - "\"Spinner\" used by some servers. -A list (ID WHAT DONE-P).") - -(eglot--define-process-var eglot--status `(:unknown nil) - "Status as declared by the server. -A list (WHAT SERIOUS-P).") - -(eglot--define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect - "If non-nil, don't autoreconnect on unexpected quit.") - -(eglot--define-process-var eglot--contact nil - "Method used to contact a server. -Either a list of strings (a shell command and arguments), or a -list of a single string of the form :") - -(eglot--define-process-var eglot--deferred-actions - (make-hash-table :test #'equal) - "Actions deferred to when server is thought to be ready.") - -(defun eglot--make-process (name managed-major-mode contact) - "Make a process from CONTACT. -NAME is a name to give the inferior process or connection. -MANAGED-MAJOR-MODE is a symbol naming a major mode. -CONTACT is as `eglot--contact'. Returns a process object." - (let* ((readable-name (format "EGLOT server (%s/%s)" name managed-major-mode)) - (buffer (get-buffer-create - (format "*%s inferior*" readable-name))) - singleton - (proc - (if (and (setq singleton (and (null (cdr contact)) (car contact))) - (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" - singleton)) - (open-network-stream readable-name - buffer - (match-string 1 singleton) - (string-to-number - (match-string 2 singleton))) - (make-process :name readable-name - :buffer buffer - :command contact - :connection-type 'pipe - :stderr (get-buffer-create (format "*%s stderr*" - name)))))) - (set-process-filter proc #'eglot--process-filter) - (set-process-sentinel proc #'eglot--process-sentinel) - proc)) - -(defmacro eglot--obj (&rest what) - "Make WHAT a suitable argument for `json-encode'." - (declare (debug (&rest form))) - ;; FIXME: maybe later actually do something, for now this just fixes - ;; the indenting of literal plists. - `(list ,@what)) - (defun eglot--project-short-name (project) "Give PROJECT a short name." (file-name-base (directory-file-name (car (project-roots project))))) @@ -200,11 +146,11 @@ CONTACT is as `eglot--contact'. Returns a process object." (defun eglot--client-capabilities () "What the EGLOT LSP client supports." - (eglot--obj - :workspace (eglot--obj + (jrpc-obj + :workspace (jrpc-obj :symbol `(:dynamicRegistration :json-false)) - :textDocument (eglot--obj - :synchronization (eglot--obj + :textDocument (jrpc-obj + :synchronization (jrpc-obj :dynamicRegistration :json-false :willSave t :willSaveWaitUntil :json-false @@ -217,49 +163,7 @@ CONTACT is as `eglot--contact'. Returns a process object." :documentHighlight `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (eglot--obj))) - -(defun eglot--connect (project managed-major-mode short-name contact interactive) - "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. -INTERACTIVE is t if inside interactive call." - (let* ((proc (eglot--make-process short-name managed-major-mode contact)) - (buffer (process-buffer proc))) - (setf (eglot--contact proc) contact - (eglot--project proc) project - (eglot--major-mode proc) managed-major-mode) - (with-current-buffer buffer - (let ((inhibit-read-only t)) - (setf (eglot--inhibit-autoreconnect proc) - (cond - ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) - (interactive nil) - ((cl-plusp eglot-autoreconnect) - (run-with-timer eglot-autoreconnect nil - (lambda () - (setf (eglot--inhibit-autoreconnect proc) - (null eglot-autoreconnect))))))) - (setf (eglot--short-name proc) short-name) - (push proc (gethash project eglot--processes-by-project)) - (erase-buffer) - (read-only-mode t) - (cl-destructuring-bind (&key capabilities) - (eglot--request - proc - :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [] - :capabilities (eglot--client-capabilities))) - (setf (eglot--capabilities proc) capabilities) - (setf (eglot--status proc) nil) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))) - (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) - proc))))) + :experimental (jrpc-obj))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -330,7 +234,7 @@ INTERACTIVE is t if called interactively." (unless project (eglot--error "Cannot work without a current project!")) (unless command (eglot--error "Don't know how to start EGLOT for %s buffers" major-mode)) - (let ((current-process (eglot--current-process))) + (let ((current-process (jrpc-current-process))) (if (and (process-live-p current-process) interactive (y-or-n-p "[eglot] Live process found, reconnect instead? ")) @@ -339,7 +243,7 @@ INTERACTIVE is t if called interactively." (eglot-shutdown current-process)) (let ((proc (eglot--connect project managed-major-mode - short-name + (format "%s/%s" short-name managed-major-mode) command interactive))) (eglot--message "Connected! Process `%s' now \ @@ -349,336 +253,56 @@ managing `%s' buffers in project `%s'." (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) + (interactive (list (jrpc-current-process-or-lose) t)) (when (process-live-p process) (eglot-shutdown process interactive)) (eglot--connect (eglot--project process) (eglot--major-mode process) - (eglot--short-name process) - (eglot--contact process) + (jrpc-name process) + (jrpc-contact process) interactive) (eglot--message "Reconnected!")) -(defun eglot--process-sentinel (proc change) - "Called when PROC undergoes CHANGE." - (eglot--log-event proc `(:message "Process state changed" :change ,change)) - (when (not (process-live-p proc)) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert "\n----------b---y---e---b---y---e----------\n"))) - ;; Cancel outstanding timers - (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success _error timeout) triplet - (cancel-timer timeout))) - (eglot--pending-continuations proc)) - (unwind-protect - ;; Call all outstanding error handlers - (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success error _timeout) triplet - (funcall error :code -1 :message (format "Server died")))) - (eglot--pending-continuations proc)) - ;; Turn off `eglot--managed-mode' where appropriate. +(defalias 'eglot-events-buffer 'jrpc-events-buffer) + +(defun eglot--connect (project managed-major-mode name command + dont-inhibit) + (let ((proc (jrpc-connect name command "eglot--server-"))) + (setf (eglot--project proc) project) + (setf (eglot--major-mode proc)managed-major-mode) + (push proc (gethash project eglot--processes-by-project)) + (cl-destructuring-bind (&key capabilities) + (jrpc-request + proc + :initialize + (jrpc-obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [] + :capabilities (eglot--client-capabilities))) + (setf (eglot--capabilities proc) capabilities) + (setf (jrpc-status proc) nil) (dolist (buffer (buffer-list)) (with-current-buffer buffer - (when (eglot--buffer-managed-p proc) - (eglot--managed-mode -1)))) - ;; Forget about the process-project relationship - (setf (gethash (eglot--project proc) eglot--processes-by-project) - (delq proc - (gethash (eglot--project proc) eglot--processes-by-project))) - (eglot--message "Server exited with status %s" (process-exit-status proc)) - (cond ((eglot--moribund proc)) - ((not (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Reconnecting unexpected server exit.") - (eglot-reconnect proc)) - (t - (eglot--warn "Not auto-reconnecting, last one didn't last long."))) - (delete-process proc)))) - -(defun eglot--process-filter (proc string) - "Called when new data STRING has arrived for PROC." - (when (buffer-live-p (process-buffer proc)) - (with-current-buffer (process-buffer proc) - (let ((inhibit-read-only t) - (expected-bytes (eglot--expected-bytes proc))) - ;; Insert the text, advancing the process marker. - ;; - (save-excursion - (goto-char (process-mark proc)) - (insert string) - (set-marker (process-mark proc) (point))) - ;; Loop (more than one message might have arrived) - ;; - (unwind-protect - (let (done) - (while (not done) - (cond - ((not expected-bytes) - ;; Starting a new message - ;; - (setq expected-bytes - (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \ -*\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t) - (string-to-number (match-string 1)))) - (unless expected-bytes - (setq done :waiting-for-new-message))) - (t - ;; Attempt to complete a message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; Process content in another buffer, - ;; shielding buffer from tamper - ;; - (with-temp-buffer - (eglot--process-receive proc json-message)))) - (goto-char message-end) - (delete-region (point-min) (point)) - (setq expected-bytes nil)))) - (t - ;; Message is still incomplete - ;; - (setq done :waiting-for-more-bytes-in-this-message)))))))) - ;; Saved parsing state for next visit to this filter - ;; - (setf (eglot--expected-bytes proc) expected-bytes)))))) - -(defun eglot-events-buffer (process &optional interactive) - "Display events buffer for current LSP connection PROCESS. -INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (let* ((probe (eglot--events-buffer process)) - (buffer (or (and (buffer-live-p probe) - probe) - (let ((buffer (get-buffer-create - (format "*%s events*" - (process-name process))))) - (with-current-buffer buffer - (buffer-disable-undo) - (read-only-mode t) - (setf (eglot--events-buffer process) buffer)) - buffer)))) - (when interactive (display-buffer buffer)) - buffer)) - -(defun eglot--log-event (proc message &optional type) - "Log an eglot-related event. -PROC is the current process. MESSAGE is a JSON-like plist. TYPE -is a symbol saying if this is a client or server originated." - (with-current-buffer (eglot-events-buffer proc) - (cl-destructuring-bind (&key method id error &allow-other-keys) message - (let* ((inhibit-read-only t) - (subtype (cond ((and method id) 'request) - (method 'notification) - (id 'reply) - (t 'message))) - (type - (format "%s-%s" (or type :internal) subtype))) - (goto-char (point-max)) - (let ((msg (format "%s%s%s:\n%s\n" - type - (if id (format " (id:%s)" id) "") - (if error " ERROR" "") - (pp-to-string message)))) - (when error - (setq msg (propertize msg 'face 'error))) - (insert-before-markers msg)))))) - -(defun eglot--process-receive (proc message) - "Process MESSAGE from PROC." - (cl-destructuring-bind (&key method id error &allow-other-keys) message - (let* ((continuations (and id - (not method) - (gethash id (eglot--pending-continuations proc))))) - (eglot--log-event proc message 'server) - (when error (setf (eglot--status proc) `(,error t))) - (cond (method - ;; a server notification or a server request - (let* ((handler-sym (intern (concat "eglot--server-" method)))) - (if (functionp handler-sym) - (apply handler-sym proc (append - (plist-get message :params) - (if id `(:id ,id)))) - (eglot--warn "No implementation of method %s yet" method) - (when id - (eglot--reply - proc id - :error (eglot--obj :code -32601 - :message "Method unimplemented")))))) - (continuations - (cancel-timer (cl-third continuations)) - (remhash id (eglot--pending-continuations proc)) - (if error - (apply (cl-second continuations) error) - (let ((res (plist-get message :result))) - (if (listp res) - (apply (cl-first continuations) res) - (funcall (cl-first continuations) res))))) - (id - (eglot--warn "Ooops no continuation for id %s" id))) - (eglot--call-deferred proc) - (force-mode-line-update t)))) - -(defvar eglot--expect-carriage-return nil) - -(defun eglot--process-send (proc message) - "Send MESSAGE to PROC (ID is optional)." - (let ((json (json-encode message))) - (process-send-string proc (format "Content-Length: %d\r\n\r\n%s" - (string-bytes json) - json)) - (eglot--log-event proc message 'client))) - -(defvar eglot--next-request-id 0) - -(defun eglot--next-request-id () - "Compute the next id for a client request." - (setq eglot--next-request-id (1+ eglot--next-request-id))) - -(defun eglot-forget-pending-continuations (process) - "Stop waiting for responses from the current LSP PROCESS." - (interactive (list (eglot--current-process-or-lose))) - (clrhash (eglot--pending-continuations process))) - -(defun eglot-clear-status (process) - "Clear most recent error message from PROCESS." - (interactive (list (eglot--current-process-or-lose))) - (setf (eglot--status process) nil)) - -(defun eglot--call-deferred (proc) - "Call PROC's deferred actions, who may again defer themselves." - (when-let ((actions (hash-table-values (eglot--deferred-actions proc)))) - (eglot--log-event proc `(:running-deferred ,(length actions))) - (mapc #'funcall (mapcar #'car actions)))) - -(defvar eglot--ready-predicates '(eglot--server-ready-p) - "Special hook of predicates controlling deferred actions. -If one of these returns nil, a deferrable `eglot--async-request' -will be deferred. Each predicate is passed the symbol for the -request request and a process object.") + (eglot--maybe-activate-editing-mode proc))) + (jrpc-notify proc :initialized (jrpc-obj :__dummy__ t)) + (setf (eglot--inhibit-autoreconnect proc) + (cond + ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) + (dont-inhibit nil) + ((cl-plusp eglot-autoreconnect) + (run-with-timer eglot-autoreconnect nil + (lambda () + (setf (eglot--inhibit-autoreconnect proc) + (null eglot-autoreconnect))))))) + proc))) (defun eglot--server-ready-p (_what _proc) "Tell if server of PROC ready for processing deferred WHAT." (not (eglot--outstanding-edits-p))) -(cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1) (debug (sexp &rest form))) - `(cl-function (lambda ,cl-lambda-list ,@body))) - -(cl-defun eglot--async-request (proc - method - params - &rest args - &key success-fn error-fn timeout-fn - (timeout eglot-request-timeout) - (deferred nil)) - "Make a request to PROCESS, expecting a reply. -Return the ID of this request. Wait TIMEOUT seconds for response. -If DEFERRED, maybe defer request to the future, or never at all, -in case a new request with identical DEFERRED and for the same -buffer overrides it. However, if that happens, the original -timeout keeps counting." - (let* ((id (eglot--next-request-id)) - (existing-timer nil) - (make-timeout - (lambda ( ) - (or existing-timer - (run-with-timer - timeout nil - (lambda () - (remhash id (eglot--pending-continuations proc)) - (funcall (or timeout-fn - (lambda () - (eglot--error - "Tired of waiting for reply to %s, id=%s" - method id)))))))))) - (when deferred - (let* ((buf (current-buffer)) - (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) - (when existing (setq existing-timer (cadr existing))) - (if (run-hook-with-args-until-failure 'eglot--ready-predicates - deferred proc) - (remhash (list deferred buf) (eglot--deferred-actions proc)) - (eglot--log-event proc `(:deferring ,method :id ,id :params ,params)) - (let* ((buf (current-buffer)) (point (point)) - (later (lambda () - (when (buffer-live-p buf) - (with-current-buffer buf - (save-excursion (goto-char point) - (apply #'eglot--async-request proc - method params args))))))) - (puthash (list deferred buf) (list later (funcall make-timeout)) - (eglot--deferred-actions proc)) - (cl-return-from eglot--async-request nil))))) - ;; Really run it - ;; - (puthash id - (list (or success-fn - (eglot--lambda (&rest _ignored) - (eglot--log-event - proc (eglot--obj :message "success ignored" :id id)))) - (or error-fn - (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status proc) `(,message t)) - proc (eglot--obj :message "error ignored, status set" - :id id :error code))) - (funcall make-timeout)) - (eglot--pending-continuations proc)) - (eglot--process-send proc (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)))) - -(defun eglot--request (proc method params &optional deferred) - "Like `eglot--async-request' for PROC, METHOD and PARAMS, but synchronous. -Meaning only return locally if successful, otherwise exit non-locally. -DEFERRED is passed to `eglot--async-request', which see." - ;; Launching a deferred sync request with outstanding changes is a - ;; bad idea, since that might lead to the request never having a - ;; chance to run, because `eglot--ready-predicates'. - (when deferred (eglot--signal-textDocument/didChange)) - (let ((retval)) - (eglot--async-request - proc method params - :success-fn (lambda (&rest args) - (setq retval `(done ,(if (vectorp (car args)) - (car args) args)))) - :error-fn (eglot--lambda (&key code message &allow-other-keys) - (setq retval `(error ,(format "Oops: %s: %s" code message)))) - :timeout-fn (lambda () - (setq retval '(error "Timed out"))) - :deferred deferred) - (while (not retval) (accept-process-output nil 30)) - (when (eq 'error (car retval)) (eglot--error (cadr retval))) - (cadr retval))) - -(cl-defun eglot--notify (process method params) - "Notify PROCESS of something, don't expect a reply.e" - (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :method method - :params params))) - -(cl-defun eglot--reply (process id &key result error) - "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send - process `(:jsonrpc "2.0" :id ,id - ,@(when result `(:result ,result)) - ,@(when error `(:error ,error))))) - ;;; Helpers ;;; @@ -701,7 +325,7 @@ DEFERRED is passed to `eglot--async-request', which see." (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion - (eglot--obj :line + (jrpc-obj :line ;; F!@(#*&#$)CKING OFF-BY-ONE (1- (line-number-at-pos pos t)) :character @@ -718,11 +342,6 @@ DEFERRED is passed to `eglot--async-request', which see." (line-beginning-position)))) (point))) - -(defun eglot--mapply (fun seq) - "Apply FUN to every element of SEQ." - (mapcar (lambda (e) (apply fun e)) seq)) - (defun eglot--path-to-uri (path) "Urify PATH." (url-hexify-string (concat "file://" (file-truename path)) @@ -759,7 +378,7 @@ DEFERRED is passed to `eglot--async-request', which see." (defun eglot--server-capable (feat) "Determine if current server is capable of FEAT." - (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) + (plist-get (eglot--capabilities (jrpc-current-process-or-lose)) feat)) (cl-defmacro eglot--with-lsp-range ((start end) range &body body &aux (range-sym (cl-gensym))) @@ -780,6 +399,9 @@ DEFERRED is passed to `eglot--async-request', which see." nil nil eglot-mode-map (cond (eglot--managed-mode + (add-hook 'jrpc-find-process-functions 'eglot--find-current-process nil t) + (add-hook 'jrpc-ready-predicates 'eglot--server-ready-p nil t) + (add-hook 'jrpc-server-moribund-hook 'eglot--on-shutdown nil t) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -793,6 +415,9 @@ DEFERRED is passed to `eglot--async-request', which see." #'eglot-eldoc-function) (add-function :around (local imenu-create-index-function) #'eglot-imenu)) (t + (remove-hook 'jrpc-find-process-functions 'eglot--find-current-process t) + (remove-hook 'jrpc-ready-predicates 'eglot--server-ready-p t) + (remove-hook 'jrpc-server-moribund-hook 'eglot--on-shutdown t) (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -805,7 +430,7 @@ DEFERRED is passed to `eglot--async-request', which see." (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) (remove-function (local imenu-create-index-function) #'eglot-imenu) - (let ((proc (eglot--current-process))) + (let ((proc (eglot--find-current-process))) (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) (eglot-shutdown proc t)))))) @@ -813,10 +438,12 @@ DEFERRED is passed to `eglot--async-request', which see." (add-hook 'eglot--managed-mode-hook 'eldoc-mode) (defun eglot--buffer-managed-p (&optional proc) - "Tell if current buffer is managed by PROC." - (and buffer-file-name (let ((cur (eglot--current-process))) - (or (and (null proc) cur) - (and proc (eq proc cur)))))) + "Tell if current buffer can be managed by PROC." + (and buffer-file-name + (cond ((null proc) (jrpc-current-process)) + (t (and (eq major-mode (eglot--major-mode proc)) + (let ((proj (project-current))) + (and proj (equal proj (eglot--project proc))))))))) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -868,12 +495,11 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((proc (eglot--current-process)) - (name (and (process-live-p proc) (eglot--short-name proc))) - (pending (and proc (hash-table-count - (eglot--pending-continuations proc)))) + (pcase-let* ((proc (jrpc-current-process)) + (name (and (process-live-p proc) (jrpc-name proc))) + (pending (and proc (length (jrpc-outstanding-request-ids proc)))) (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) - (`(,status ,serious-p) (and proc (eglot--status proc)))) + (`(,status ,serious-p) (and proc (jrpc-status proc)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line '((down-mouse-1 eglot-menu "pop up EGLOT menu")))) @@ -908,25 +534,6 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (proc &optional interactive) - "Politely ask the server PROC to quit. -Forcefully quit it if it doesn't respond. Don't leave this -function with the server still running. INTERACTIVE is t if -called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (when interactive (eglot--message "Asking %s politely to terminate" proc)) - (unwind-protect - (let ((eglot-request-timeout 3)) - (setf (eglot--moribund proc) t) - (eglot--request proc - :shutdown - nil) - ;; this one should always fail - (ignore-errors (eglot--request proc :exit nil))) - (when (process-live-p proc) - (eglot--warn "Brutally deleting existing process %s" proc) - (delete-process proc)))) - (cl-defun eglot--server-window/showMessage (_process &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" @@ -949,10 +556,10 @@ called interactively." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (eglot--reply process id :result (eglot--obj :title reply)) - (eglot--reply process id - :error (eglot--obj :code -32800 - :message "User cancelled")))))) + (jrpc-reply process id :result (jrpc-obj :title reply)) + (jrpc-reply process id + :error (jrpc-obj :code -32800 + :message "User cancelled")))))) (cl-defun eglot--server-window/logMessage (_proc &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer @@ -978,12 +585,12 @@ called interactively." _code source message) diag-spec (eglot--with-lsp-range (beg end) range - (flymake-make-diagnostic (current-buffer) - beg end - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message)))) + (flymake-make-diagnostic (current-buffer) + beg end + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message)))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -996,7 +603,7 @@ called interactively." (cl-defun eglot--server-client/registerCapability (proc &key id registrations) "Handle notification client/registerCapability" - (let ((jsonrpc-id id) + (let ((jrpc-id id) (done (make-symbol "done"))) (catch done (mapc @@ -1012,13 +619,13 @@ called interactively." (apply handler-sym proc :id id registerOptions)))) (unless ok (throw done - (eglot--reply proc jsonrpc-id - :error (eglot--obj - :code -32601 - :message (or message "sorry :-(")))))))) + (jrpc-reply proc jrpc-id + :error (jrpc-obj + :code -32601 + :message (or message "sorry :-(")))))))) reg)) registrations) - (eglot--reply proc id :result (eglot--obj :message "OK"))))) + (jrpc-reply proc id :result (jrpc-obj :message "OK"))))) (cl-defun eglot--server-workspace/applyEdit (proc &key id _label edit) @@ -1026,30 +633,30 @@ called interactively." (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) - (eglot--reply proc id :result `(:applied ))) + (jrpc-reply proc id :result `(:applied ))) (error - (eglot--reply proc id - :result `(:applied :json-false) - :error - (eglot--obj :code -32001 - :message (format "%s" err)))))) + (jrpc-reply proc id + :result `(:applied :json-false) + :error + (jrpc-obj :code -32001 + :message (format "%s" err)))))) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." - (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) + (jrpc-obj :uri (eglot--path-to-uri buffer-file-name))) (defvar-local eglot--versioned-identifier 0) (defun eglot--VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (append (eglot--TextDocumentIdentifier) - (eglot--obj :version eglot--versioned-identifier))) + (jrpc-obj :version eglot--versioned-identifier))) (defun eglot--TextDocumentItem () "Compute TextDocumentItem object for current buffer." (append (eglot--VersionedTextDocumentIdentifier) - (eglot--obj :languageId + (jrpc-obj :languageId (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) (match-string 1 (symbol-name major-mode)) "unknown") @@ -1060,7 +667,7 @@ called interactively." (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." - (eglot--obj :textDocument (eglot--TextDocumentIdentifier) + (jrpc-obj :textDocument (eglot--TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil @@ -1091,10 +698,16 @@ Records START, END and PRE-CHANGE-LENGTH locally." `[(,pre-change-length ,(buffer-substring-no-properties start end))]))) +;; HACK! +(advice-add #'jrpc-request :before + (lambda (_proc _method _params &optional deferred) + (when (and eglot--managed-mode deferred) + (eglot--signal-textDocument/didChange)))) + (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when (eglot--outstanding-edits-p) - (let* ((proc (eglot--current-process-or-lose)) + (let* ((proc (jrpc-current-process-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) (emacs-messup (/= (length (car eglot--recent-changes)) (length (cdr eglot--recent-changes)))) @@ -1103,56 +716,57 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--warn "`eglot--recent-changes' messup: %s" eglot--recent-changes)) (save-restriction (widen) - (eglot--notify + (jrpc-notify proc :textDocument/didChange - (eglot--obj + (jrpc-obj :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector - (eglot--obj + (jrpc-obj :text (buffer-substring-no-properties (point-min) (point-max)))) (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) for (len after-text) across (cdr eglot--recent-changes) - vconcat `[,(eglot--obj :range (eglot--obj :start start-pos - :end end-pos) - :rangeLength len - :text after-text)]))))) + vconcat `[,(jrpc-obj :range (jrpc-obj :start start-pos + :end end-pos) + :rangeLength len + :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) (setf (eglot--spinner proc) (list nil :textDocument/didChange t)) - (eglot--call-deferred proc)))) + ;; HACK! + (jrpc--call-deferred proc)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (setq eglot--recent-changes (cons [] [])) - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--TextDocumentItem)))) + (jrpc-notify (jrpc-current-process-or-lose) + :textDocument/didOpen + (jrpc-obj :textDocument + (eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didClose - (eglot--obj :textDocument - (eglot--TextDocumentIdentifier)))) + (jrpc-notify (jrpc-current-process-or-lose) + :textDocument/didClose + (jrpc-obj :textDocument + (eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (eglot--notify - (eglot--current-process-or-lose) + (jrpc-notify + (jrpc-current-process-or-lose) :textDocument/willSave - (eglot--obj + (jrpc-obj :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha :textDocument (eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." - (eglot--notify - (eglot--current-process-or-lose) + (jrpc-notify + (jrpc-current-process-or-lose) :textDocument/didSave - (eglot--obj + (jrpc-obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) :textDocument (eglot--TextDocumentIdentifier)))) @@ -1192,26 +806,26 @@ DUMMY is ignored" (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) - (let ((proc (eglot--current-process-or-lose)) + (let ((proc (jrpc-current-process-or-lose)) (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols - (eglot--mapply - (eglot--lambda (&key name kind location containerName) + (jrpc-mapply + (jrpc-lambda (&key name kind location containerName) (propertize name :textDocumentPositionParams - (eglot--obj :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) + (jrpc-obj :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) :locations (list location) :kind kind :containerName containerName)) - (eglot--request proc - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) + (jrpc-request proc + :textDocument/documentSymbol + (jrpc-obj + :textDocument text-id)))) (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) @@ -1226,12 +840,12 @@ DUMMY is ignored" (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (eglot--request (eglot--current-process-or-lose) - :textDocument/definition - (get-text-property - 0 :textDocumentPositionParams identifier))))) - (eglot--mapply - (eglot--lambda (&key uri range) + (jrpc-request (jrpc-current-process-or-lose) + :textDocument/definition + (get-text-property + 0 :textDocumentPositionParams identifier))))) + (jrpc-mapply + (jrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) location-or-locations))) @@ -1244,43 +858,43 @@ DUMMY is ignored" (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--mapply - (eglot--lambda (&key uri range) + (jrpc-mapply + (jrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (eglot--request (eglot--current-process-or-lose) - :textDocument/references - (append - params - (eglot--obj :context - (eglot--obj :includeDeclaration t))))))) + (jrpc-request (jrpc-current-process-or-lose) + :textDocument/references + (append + params + (jrpc-obj :context + (jrpc-obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (eglot--mapply - (eglot--lambda (&key name location &allow-other-keys) + (jrpc-mapply + (jrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) - (eglot--request (eglot--current-process-or-lose) - :workspace/symbol - (eglot--obj :query pattern))))) + (jrpc-request (jrpc-current-process-or-lose) + :workspace/symbol + (jrpc-obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (proc (eglot--current-process-or-lose))) + (proc (jrpc-current-process-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) (completion-table-with-cache (lambda (_ignored) - (let* ((resp (eglot--request proc - :textDocument/completion - (eglot--TextDocumentPositionParams) - :textDocument/completion)) + (let* ((resp (jrpc-request proc + :textDocument/completion + (eglot--TextDocumentPositionParams) + :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (eglot--mapply - (eglot--lambda (&rest all &key label &allow-other-keys) + (jrpc-mapply + (jrpc-lambda (&rest all &key label &allow-other-keys) (add-text-properties 0 1 all label) label) items)))) :annotation-function @@ -1299,8 +913,8 @@ DUMMY is ignored" (lambda (obj) (let ((documentation (or (get-text-property 0 :documentation obj) - (plist-get (eglot--request proc :completionItem/resolve - (text-properties-at 0 obj)) + (plist-get (jrpc-request proc :completionItem/resolve + (text-properties-at 0 obj)) :documentation)))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") @@ -1317,7 +931,7 @@ DUMMY is ignored" (defun eglot--hover-info (contents &optional range) (concat (and range (eglot--with-lsp-range (beg end) range - (concat (buffer-substring beg end) ": "))) + (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append (cond ((vectorp contents) @@ -1329,8 +943,8 @@ DUMMY is ignored" "Request \"hover\" information for the thing at point." (interactive) (cl-destructuring-bind (&key contents range) - (eglot--request (eglot--current-process-or-lose) :textDocument/hover - (eglot--TextDocumentPositionParams)) + (jrpc-request (jrpc-current-process-or-lose) :textDocument/hover + (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) (with-help-window "*eglot help*" (with-current-buffer standard-output @@ -1339,26 +953,26 @@ DUMMY is ignored" (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." (let ((buffer (current-buffer)) - (proc (eglot--current-process-or-lose)) + (proc (jrpc-current-process-or-lose)) (position-params (eglot--TextDocumentPositionParams))) (when (eglot--server-capable :hoverProvider) - (eglot--async-request + (jrpc-async-request proc :textDocument/hover position-params - :success-fn (eglot--lambda (&key contents range) + :success-fn (jrpc-lambda (&key contents range) (when (get-buffer-window buffer) (with-current-buffer buffer (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) - (eglot--async-request + (jrpc-async-request proc :textDocument/documentHighlight position-params :success-fn (lambda (highlights) (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights (when (get-buffer-window buffer) (with-current-buffer buffer - (eglot--mapply - (eglot--lambda (&key range _kind) + (jrpc-mapply + (jrpc-lambda (&key range _kind) (eglot--with-lsp-range (beg end) range (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) @@ -1372,15 +986,15 @@ DUMMY is ignored" "EGLOT's `imenu-create-index-function' overriding OLDFUN." (if (eglot--server-capable :documentSymbolProvider) (let ((entries - (eglot--mapply - (eglot--lambda (&key name kind location _containerName) + (jrpc-mapply + (jrpc-lambda (&key name kind location _containerName) (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (eglot--request (eglot--current-process-or-lose) - :textDocument/documentSymbol - (eglot--obj - :textDocument (eglot--TextDocumentIdentifier)))))) + (jrpc-request (jrpc-current-process-or-lose) + :textDocument/documentSymbol + (jrpc-obj + :textDocument (eglot--TextDocumentIdentifier)))))) (append (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries) @@ -1394,8 +1008,8 @@ DUMMY is ignored" (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" buffer version eglot--versioned-identifier)) - (eglot--mapply - (eglot--lambda (&key range newText) + (jrpc-mapply + (jrpc-lambda (&key range newText) (save-restriction (widen) (save-excursion @@ -1448,9 +1062,9 @@ Proceed? " (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (eglot--request (eglot--current-process-or-lose) - :textDocument/rename `(,@(eglot--TextDocumentPositionParams) - ,@(eglot--obj :newName newname))) + (jrpc-request (jrpc-current-process-or-lose) + :textDocument/rename `(,@(eglot--TextDocumentPositionParams) + ,@(jrpc-obj :newName newname))) current-prefix-arg)) @@ -1478,7 +1092,7 @@ Proceed? " (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) (defun eglot--setup-rls-idiosyncrasies () "Prepare `eglot' to deal with RLS's special treatment." - (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) + (add-hook 'jrpc-ready-predicates 'eglot--rls-probably-ready-for-p t t))) (cl-defun eglot--server-window/progress (process &key id done title message &allow-other-keys) From dd4d81696ebcc531c8f59242537993b36654501f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 12 May 2018 22:05:20 +0100 Subject: [PATCH 129/771] Fix copyright header. obviously not since 2003 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6f9034942d5..f6eabe16861 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,6 +1,6 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2003-2018 Free Software Foundation, Inc. +;; Copyright (C) 2018 Free Software Foundation, Inc. ;; Version: 0.1 ;; Author: João Távora From c8bed8412292ac108c4a5f6ac5991e30bf643e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 13 May 2018 20:35:45 +0100 Subject: [PATCH 130/771] Ask server for textdocument/signaturehelp if it supports it * eglot.el (eglot--client-capabilities): Capable of signature Help. (eglot--sig-info): Helper for eglot-eldoc-function. (eglot-eldoc-function): Send textDocument/signatureHelp * README.md: Update to mention textDocument/signatureHelp --- lisp/progmodes/eglot.el | 97 +++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f6eabe16861..2e872dc5237 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -211,6 +211,7 @@ CONTACT is as `eglot--contact'. Returns a process object." :didSave t) :completion `(:dynamicRegistration :json-false) :hover `(:dynamicRegistration :json-false) + :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) :documentSymbol `(:dynamicRegistration :json-false) @@ -1330,6 +1331,28 @@ DUMMY is ignored" (contents (list contents)))) "\n"))) +(defun eglot--sig-info (sigs active-sig active-param) + (cl-loop + for (sig . moresigs) on (append sigs nil) for i from 0 + concat (cl-destructuring-bind (&key label _documentation parameters) sig + (let (active-doc) + (concat + (propertize (replace-regexp-in-string "(.*$" "(" label) + 'face 'font-lock-function-name-face) + (cl-loop + for (param . moreparams) on (append parameters nil) for j from 0 + concat (cl-destructuring-bind (&key label documentation) param + (when (and (eql j active-param) (eql i active-sig)) + (setq label (propertize + label + 'face 'eldoc-highlight-function-argument)) + (when documentation + (setq active-doc (concat label ": " documentation)))) + label) + if moreparams concat ", " else concat ")") + (when active-doc (concat "\n" active-doc))))) + when moresigs concat "\n")) + (defun eglot-help-at-point () "Request \"hover\" information for the thing at point." (interactive) @@ -1342,35 +1365,51 @@ DUMMY is ignored" (insert (eglot--hover-info contents range)))))) (defun eglot-eldoc-function () - "EGLOT's `eldoc-documentation-function' function." - (let ((buffer (current-buffer)) - (proc (eglot--current-process-or-lose)) - (position-params (eglot--TextDocumentPositionParams))) - (when (eglot--server-capable :hoverProvider) - (eglot--async-request - proc :textDocument/hover position-params - :success-fn (eglot--lambda (&key contents range) - (when (get-buffer-window buffer) - (with-current-buffer buffer - (eldoc-message (eglot--hover-info contents range))))) - :deferred :textDocument/hover)) - (when (eglot--server-capable :documentHighlightProvider) - (eglot--async-request - proc :textDocument/documentHighlight position-params - :success-fn (lambda (highlights) - (mapc #'delete-overlay eglot--highlights) - (setq eglot--highlights - (when (get-buffer-window buffer) - (with-current-buffer buffer - (eglot--mapply - (eglot--lambda (&key range _kind) - (eglot--with-lsp-range (beg end) range - (let ((ov (make-overlay beg end))) - (overlay-put ov 'face 'highlight) - (overlay-put ov 'evaporate t) - ov))) - highlights))))) - :deferred :textDocument/documentHighlight))) + "EGLOT's `eldoc-documentation-function' function. +If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." + (let* ((buffer (current-buffer)) + (proc (eglot--current-process-or-lose)) + (position-params (eglot--TextDocumentPositionParams)) + sig-showing) + (cl-macrolet ((when-buffer-window + (&body body) `(when (get-buffer-window buffer) + (with-current-buffer buffer ,@body)))) + (when (eglot--server-capable :signatureHelpProvider) + (eglot--async-request + proc :textDocument/signatureHelp position-params + :success-fn (eglot--lambda (&key signatures activeSignature + activeParameter) + (when-buffer-window + (when (cl-plusp (length signatures)) + (setq sig-showing t) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter))))) + :deferred :textDocument/signatureHelp)) + (when (eglot--server-capable :hoverProvider) + (eglot--async-request + proc :textDocument/hover position-params + :success-fn (eglot--lambda (&key contents range) + (unless sig-showing + (when-buffer-window + (eldoc-message (eglot--hover-info contents range))))) + :deferred :textDocument/hover)) + (when (eglot--server-capable :documentHighlightProvider) + (eglot--async-request + proc :textDocument/documentHighlight position-params + :success-fn (lambda (highlights) + (mapc #'delete-overlay eglot--highlights) + (setq eglot--highlights + (when-buffer-window + (eglot--mapply + (eglot--lambda (&key range _kind) + (eglot--with-lsp-range (beg end) 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 (oldfun) From bf1365c4f8fcb989bb6907c2cec57edab22cef60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 13 May 2018 22:03:32 +0100 Subject: [PATCH 131/771] Work with any old directory, no formal project needed Actually, uses a "transient project" which project-current returns if desperate. * README.md: Update * eglot.el (eglot--current-process) (eglot--current-process-or-lose): Simplify. (eglot): Maybe prompt user for project. --- lisp/progmodes/eglot.el | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e872dc5237..c1b63ea6e3d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -24,8 +24,7 @@ ;;; Commentary: -;; M-x eglot in some file under some .git controlled dir should get -;; you started, but see README.md. +;; Simply M-x eglot should be enough to get you started, but see README.md. ;;; Code: @@ -79,15 +78,13 @@ lasted more than that many seconds." (defun eglot--current-process () "The current logical EGLOT process." - (let* ((cur (project-current)) - (processes (and cur (gethash cur eglot--processes-by-project)))) - (cl-find major-mode processes :key #'eglot--major-mode))) + (let* ((probe (or (project-current) (cons 'transient default-directory)))) + (cl-find major-mode (gethash probe eglot--processes-by-project) + :key #'eglot--major-mode))) (defun eglot--current-process-or-lose () "Return the current EGLOT process or error." - (or (eglot--current-process) - (eglot--error "No current EGLOT process%s" - (if (project-current) "" " (Also no current project)")))) + (or (eglot--current-process) (eglot--error "No current EGLOT process"))) (defmacro eglot--define-process-var (var-sym initval &optional doc) @@ -326,11 +323,8 @@ MANAGED-MAJOR-MODE. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) - (let* ((project (project-current)) + (let* ((project (project-current 'maybe)) (short-name (eglot--project-short-name project))) - (unless project (eglot--error "Cannot work without a current project!")) - (unless command (eglot--error "Don't know how to start EGLOT for %s buffers" - major-mode)) (let ((current-process (eglot--current-process))) (if (and (process-live-p current-process) interactive From 9d0984c0cde87e71cea4729f6e6ca63bfbfa5814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 13 May 2018 23:22:31 +0100 Subject: [PATCH 132/771] Fix automatic project creation * eglot.el (eglot): Take PROJECT arg. Return process. (eglot--interactive): Returns a project. --- lisp/progmodes/eglot.el | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c1b63ea6e3d..3d5d4927ece 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -292,6 +292,7 @@ INTERACTIVE is t if inside interactive call." "\n" base-prompt))))) (list managed-mode + (or (project-current) `(transient . default-directory)) (if prompt (split-string-and-unquote (read-shell-command prompt @@ -302,11 +303,13 @@ INTERACTIVE is t if inside interactive call." t))) ;;;###autoload -(defun eglot (managed-major-mode command &optional interactive) +(defun eglot (managed-major-mode project command &optional interactive) "Start a Language Server Protocol server. Server is started with COMMAND and manages buffers of MANAGED-MAJOR-MODE for the current project. +PROJECT is a project instance as returned by `project-current'. + COMMAND is a list of strings, an executable program and optionally its arguments. If the first and only string in the list is of the form \":\" it is taken as an @@ -323,8 +326,7 @@ MANAGED-MAJOR-MODE. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) - (let* ((project (project-current 'maybe)) - (short-name (eglot--project-short-name project))) + (let* ((short-name (eglot--project-short-name project))) (let ((current-process (eglot--current-process))) (if (and (process-live-p current-process) interactive @@ -339,7 +341,8 @@ INTERACTIVE is t if called interactively." interactive))) (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." - proc managed-major-mode short-name)))))) + proc managed-major-mode short-name) + proc))))) (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. From 33583c642a3ff08d94c2226fa4df7b2204a1e3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 13 May 2018 23:25:15 +0100 Subject: [PATCH 133/771] Use rls in travis ci and add actual tests Also run a hook when connected * eglot-tests.el (eglot--with-dirs-and-files) (eglot--make-file-or-dirs, eglot--call-with-dirs-and-files) (eglot--find-file-noselect): New helpers. (auto-detect-running-server, auto-reconnect): New actual tests. * eglot.el (eglot-connect): Run hook when connected (eglot-connect-hook): New variable * .travis.yml: Use rust stable and install rls * README.md: Update mention of automated tests --- lisp/progmodes/eglot.el | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d5d4927ece..447724283cf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -217,6 +217,8 @@ CONTACT is as `eglot--contact'. Returns a process object." :publishDiagnostics `(:relatedInformation :json-false)) :experimental (eglot--obj))) +(defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") + (defun eglot--connect (project managed-major-mode short-name contact interactive) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. INTERACTIVE is t if inside interactive call." @@ -238,6 +240,7 @@ INTERACTIVE is t if inside interactive call." (null eglot-autoreconnect))))))) (setf (eglot--short-name proc) short-name) (push proc (gethash project eglot--processes-by-project)) + (run-hook-with-args 'eglot-connect-hook proc) (erase-buffer) (read-only-mode t) (cl-destructuring-bind (&key capabilities) From 188cd6da28f271144fb88b9280532f18aec0b3dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 10:50:49 +0100 Subject: [PATCH 134/771] Don't define a menu if nothing to show there for now * eglot.el (eglot-menu): Remove it. (eglot--mode-line-format): Don't define a menu. --- lisp/progmodes/eglot.el | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 447724283cf..6dc22135550 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -843,10 +843,6 @@ that case, also signal textDocument/didOpen." ;;; Mode-line, menu and other sugar ;;; -(defvar eglot-menu) - -(easy-menu-define eglot-menu eglot-mode-map "EGLOT" `("EGLOT" )) - (defvar eglot--mode-line-format `(:eval (eglot--mode-line-format))) (put 'eglot--mode-line-format 'risky-local-variable t) @@ -881,8 +877,7 @@ Uses THING, FACE, DEFS and PREPEND." (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) (`(,status ,serious-p) (and proc (eglot--status proc)))) (append - `(,(eglot--mode-line-props "eglot" 'eglot-mode-line - '((down-mouse-1 eglot-menu "pop up EGLOT menu")))) + `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) (when name `(":" ,(eglot--mode-line-props name 'eglot-mode-line From 92efbb8dac5b4443445ecc16ea3455aa609ded82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 11:20:37 +0100 Subject: [PATCH 135/771] Now send willsavewaituntil * eglot.el (eglot--client-capabilities): Report willSaveWaitUntil. (eglot--server-workspace/applyEdit): Fix docstring. (eglot--signal-textDocument/willSave): Send willSaveWaitUntil (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didClose): Don't eglot--obj. (eglot--apply-text-edits): Simplify. Use current buffer. (eglot--apply-workspace-edit): Use new eglot--apply-text-edits. --- lisp/progmodes/eglot.el | 70 +++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6dc22135550..eab7ce71113 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -203,9 +203,7 @@ CONTACT is as `eglot--contact'. Returns a process object." :textDocument (eglot--obj :synchronization (eglot--obj :dynamicRegistration :json-false - :willSave t - :willSaveWaitUntil :json-false - :didSave t) + :willSave t :willSaveWaitUntil t :didSave t) :completion `(:dynamicRegistration :json-false) :hover `(:dynamicRegistration :json-false) :signatureHelp `(:dynamicRegistration :json-false) @@ -1023,7 +1021,7 @@ called interactively." (cl-defun eglot--server-workspace/applyEdit (proc &key id _label edit) - "Handle notification client/registerCapability" + "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) @@ -1127,26 +1125,27 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (setq eglot--recent-changes (cons [] [])) - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didOpen - (eglot--obj :textDocument - (eglot--TextDocumentItem)))) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (eglot--notify (eglot--current-process-or-lose) - :textDocument/didClose - (eglot--obj :textDocument - (eglot--TextDocumentIdentifier)))) + (eglot--notify + (eglot--current-process-or-lose) + :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (eglot--notify - (eglot--current-process-or-lose) - :textDocument/willSave - (eglot--obj - :reason 1 ; Manual, emacs laughs in the face of auto-save muahahahaha - :textDocument (eglot--TextDocumentIdentifier)))) + (let ((proc (eglot--current-process-or-lose)) + (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) + (eglot--notify proc :textDocument/willSave params) + (ignore-errors + (let ((eglot-request-timeout 0.5)) + (when (plist-get :willSaveWaitUntil + (eglot--server-capable :textDocumentSync)) + (eglot--apply-text-edits + (eglot--request proc :textDocument/willSaveWaituntil params))))))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." @@ -1426,22 +1425,20 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." entries)) (funcall oldfun))) -(defun eglot--apply-text-edits (buffer edits &optional version) - "Apply the EDITS for BUFFER." - (with-current-buffer buffer - (unless (or (not version) - (equal version eglot--versioned-identifier)) - (eglot--error "Edits on `%s' require version %d, you have %d" - buffer version eglot--versioned-identifier)) - (eglot--mapply - (eglot--lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (eglot--with-lsp-range (beg end) range - (goto-char beg) (delete-region beg end) (insert newText))))) - edits) - (eglot--message "%s: Performed %s edits" (current-buffer) (length edits)))) +(defun eglot--apply-text-edits (edits &optional version) + "Apply EDITS for current buffer if at VERSION, or if it's nil." + (unless (or (not version) (equal version eglot--versioned-identifier)) + (eglot--error "Edits on `%s' require version %d, you have %d" + (current-buffer) version eglot--versioned-identifier)) + (eglot--mapply + (eglot--lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (eglot--with-lsp-range (beg end) range + (goto-char beg) (delete-region beg end) (insert newText))))) + edits) + (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) "Apply the workspace edit WEDIT. If CONFIRM, ask user first." @@ -1471,9 +1468,8 @@ Proceed? " (let (edit) (while (setq edit (car prepared)) (cl-destructuring-bind (path edits &optional version) edit - (eglot--apply-text-edits (find-file-noselect path) - edits - version) + (with-current-buffer (find-file-noselect path) + (eglot--apply-text-edits edits version)) (pop prepared)))) (if prepared (eglot--warn "Caution: edits of files %s failed." From d0e32ae98cd3680cf0b91e3f37a53a8e4fdea843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 11:41:46 +0100 Subject: [PATCH 136/771] Remove an unused variable * eglot.el (eglot--expect-carriage-return): Get rid of this. --- lisp/progmodes/eglot.el | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eab7ce71113..9b0f290af50 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -532,8 +532,6 @@ is a symbol saying if this is a client or server originated." (eglot--call-deferred proc) (force-mode-line-update t)))) -(defvar eglot--expect-carriage-return nil) - (defun eglot--process-send (proc message) "Send MESSAGE to PROC (ID is optional)." (let ((json (json-encode message))) @@ -542,7 +540,7 @@ is a symbol saying if this is a client or server originated." json)) (eglot--log-event proc message 'client))) -(defvar eglot--next-request-id 0) +(defvar eglot--next-request-id 0 "ID for next request.") (defun eglot--next-request-id () "Compute the next id for a client request." From c511228cdaedbde8136847dbfe576ad1473d9aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 14:18:18 +0100 Subject: [PATCH 137/771] Support didchangewatchedfiles with dynamic registration RLS uses this, presumaly for knowing about Cargo.toml changes and stuff. * README.md: Update protocol compliance. * eglot.el (filenotify): Require it. (eglot--file-watches): New process-local var. (eglot--process-sentinel): Kill all watches (eglot--register-unregister): New helper. (eglot--server-client/registerCapability): Simplify. (eglot--server-client/unregisterCapability): New method. (eglot--register-workspace/didChangeWatchedFiles) (eglot--unregister-workspace/didChangeWatchedFiles): New capability. (eglot--client-capabilities): Update. --- lisp/progmodes/eglot.el | 102 ++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 30 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9b0f290af50..85b2d89a67a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -39,6 +39,7 @@ (require 'flymake) (require 'xref) (require 'subr-x) +(require 'filenotify) ;;; User tweakable stuff @@ -148,6 +149,9 @@ list of a single string of the form :") (make-hash-table :test #'equal) "Actions deferred to when server is thought to be ready.") +(eglot--define-process-var eglot--file-watches (make-hash-table :test #'equal) + "File system watches for the didChangeWatchedfiles thingy.") + (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is a name to give the inferior process or connection. @@ -199,6 +203,9 @@ CONTACT is as `eglot--contact'. Returns a process object." "What the EGLOT LSP client supports." (eglot--obj :workspace (eglot--obj + :applyEdit t + :workspaceEdit `(:documentChanges :json-false) + :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) :textDocument (eglot--obj :synchronization (eglot--obj @@ -365,11 +372,14 @@ INTERACTIVE is t if called interactively." (with-current-buffer (eglot-events-buffer proc) (let ((inhibit-read-only t)) (insert "\n----------b---y---e---b---y---e----------\n"))) - ;; Cancel outstanding timers + ;; Cancel outstanding timers and file system watches (maphash (lambda (_id triplet) (cl-destructuring-bind (_success _error timeout) triplet (cancel-timer timeout))) (eglot--pending-continuations proc)) + (maphash (lambda (_id watches) + (mapcar #'file-notify-rm-watch watches)) + (eglot--file-watches proc)) (unwind-protect ;; Call all outstanding error handlers (maphash (lambda (_id triplet) @@ -990,32 +1000,31 @@ called interactively." (t (eglot--message "OK so %s isn't visited" filename))))) +(cl-defun eglot--register-unregister (proc jsonrpc-id things how) + "Helper for `eglot--server-client/registerCapability'. +THINGS are either registrations or unregisterations." + (dolist (thing (cl-coerce things 'list)) + (cl-destructuring-bind (&key id method registerOptions) thing + (let (retval) + (unwind-protect + (setq retval (apply (intern (format "eglot--%s-%s" how method)) + proc :id id registerOptions)) + (unless (eq t (car retval)) + (cl-return-from eglot--register-unregister + (eglot--reply + proc jsonrpc-id + :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) + (eglot--reply proc jsonrpc-id :result (eglot--obj :message "OK"))) + (cl-defun eglot--server-client/registerCapability (proc &key id registrations) - "Handle notification client/registerCapability" - (let ((jsonrpc-id id) - (done (make-symbol "done"))) - (catch done - (mapc - (lambda (reg) - (apply - (cl-function - (lambda (&key id method registerOptions) - (pcase-let* - ((handler-sym (intern (concat "eglot--register-" - method))) - (`(,ok ,message) - (and (functionp handler-sym) - (apply handler-sym proc :id id registerOptions)))) - (unless ok - (throw done - (eglot--reply proc jsonrpc-id - :error (eglot--obj - :code -32601 - :message (or message "sorry :-(")))))))) - reg)) - registrations) - (eglot--reply proc id :result (eglot--obj :message "OK"))))) + "Handle server request client/registerCapability" + (eglot--register-unregister proc id registrations 'register)) + +(cl-defun eglot--server-client/unregisterCapability + (proc &key id unregisterations) ;; XXX: Yeah, typo and all.. See spec... + "Handle server request client/unregisterCapability" + (eglot--register-unregister proc id unregisterations 'unregister)) (cl-defun eglot--server-workspace/applyEdit (proc &key id _label edit) @@ -1489,12 +1498,45 @@ Proceed? " ;;; Dynamic registration ;;; -(cl-defun eglot--register-workspace/didChangeWatchedFiles - (_proc &key _id _watchers) +(cl-defun eglot--register-workspace/didChangeWatchedFiles (proc &key id watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" - ;; TODO: file-notify-add-watch and - ;; file-notify-rm-watch can probably handle this - (list nil "Sorry, can't do this yet")) + (eglot--unregister-workspace/didChangeWatchedFiles proc :id id) + (let* (success + (globs (mapcar (lambda (w) (plist-get w :globPattern)) watchers))) + (cl-labels + ((handle-event + (event) + (cl-destructuring-bind (desc action file &optional file1) event + (cond + ((and (memq action '(created changed deleted)) + (cl-find file globs + :test (lambda (f glob) + (string-match (wildcard-to-regexp + (expand-file-name glob)) + f)))) + (eglot--notify + proc :workspace/didChangeWatchedFiles + `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) + :type ,(cl-case action + (created 1) + (changed 2) + (deleted 3))))))) + ((eq action 'renamed) + (handle-event desc 'deleted file) + (handle-event desc 'created file1)))))) + (unwind-protect + (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) + (push (file-notify-add-watch dir '(change) #'handle-event) + (gethash id (eglot--file-watches proc)))) + (setq success `(t "OK"))) + (unless success + (eglot--unregister-workspace/didChangeWatchedFiles proc :id id)))))) + +(cl-defun eglot--unregister-workspace/didChangeWatchedFiles (proc &key id) + "Handle dynamic unregistration of workspace/didChangeWatchedFiles" + (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches proc))) + (remhash id (eglot--file-watches proc)) + (list t "OK")) ;;; Rust-specific From 35dae7034b96fe0f32bdd51377e9a2574adea374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 19:19:12 +0100 Subject: [PATCH 138/771] Proper server shutdown when jrpc.el is used The shutdown hook can't be a buffer-local thing, it has to be a server property. Also, on shutdown in eglot.el, remember to first unmanage buffers and only then affect eglot--processes-by-project. * eglot.el (eglot--on-shutdown): reverse order of first two sexps. (eglot--connect): Pass a shutdown function to jrpc-connect (eglot--managed-mode): Don't use jrpc-server-moribund-hook (eglot--buffer-managed-p): Simplify. Use eglot--find-current-process. --- lisp/progmodes/eglot.el | 59 +++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 934270c43f1..f33a851eced 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -95,13 +95,14 @@ A list (ID WHAT DONE-P).") (defun eglot--on-shutdown (proc) ;; Turn off `eglot--managed-mode' where appropriate. - (setf (gethash (eglot--project proc) eglot--processes-by-project) - (delq proc - (gethash (eglot--project proc) eglot--processes-by-project))) (dolist (buffer (buffer-list)) (with-current-buffer buffer (when (eglot--buffer-managed-p proc) (eglot--managed-mode -1)))) + ;; Sever the project/process relationship for proc + (setf (gethash (eglot--project proc) eglot--processes-by-project) + (delq proc + (gethash (eglot--project proc) eglot--processes-by-project))) (cond ((eglot--moribund proc)) ((not (eglot--inhibit-autoreconnect proc)) (eglot--warn "Reconnecting unexpected server exit.") @@ -267,7 +268,7 @@ INTERACTIVE is t if called interactively." (defun eglot--connect (project managed-major-mode name command dont-inhibit) - (let ((proc (jrpc-connect name command "eglot--server-"))) + (let ((proc (jrpc-connect name command "eglot--server-" #'eglot--on-shutdown))) (setf (eglot--project proc) project) (setf (eglot--major-mode proc)managed-major-mode) (push proc (gethash project eglot--processes-by-project)) @@ -326,11 +327,11 @@ INTERACTIVE is t if called interactively." "Convert point POS to LSP position." (save-excursion (jrpc-obj :line - ;; F!@(#*&#$)CKING OFF-BY-ONE - (1- (line-number-at-pos pos t)) - :character - (- (goto-char (or pos (point))) - (line-beginning-position))))) + ;; F!@(#*&#$)CKING OFF-BY-ONE + (1- (line-number-at-pos pos t)) + :character + (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist) "Convert LSP position POS-PLIST to Emacs point." @@ -401,7 +402,6 @@ INTERACTIVE is t if called interactively." (eglot--managed-mode (add-hook 'jrpc-find-process-functions 'eglot--find-current-process nil t) (add-hook 'jrpc-ready-predicates 'eglot--server-ready-p nil t) - (add-hook 'jrpc-server-moribund-hook 'eglot--on-shutdown nil t) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -417,7 +417,6 @@ INTERACTIVE is t if called interactively." (t (remove-hook 'jrpc-find-process-functions 'eglot--find-current-process t) (remove-hook 'jrpc-ready-predicates 'eglot--server-ready-p t) - (remove-hook 'jrpc-server-moribund-hook 'eglot--on-shutdown t) (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -439,11 +438,9 @@ INTERACTIVE is t if called interactively." (defun eglot--buffer-managed-p (&optional proc) "Tell if current buffer can be managed by PROC." - (and buffer-file-name - (cond ((null proc) (jrpc-current-process)) - (t (and (eq major-mode (eglot--major-mode proc)) - (let ((proj (project-current))) - (and proj (equal proj (eglot--project proc))))))))) + (and buffer-file-name (let ((cur (eglot--find-current-process))) + (or (and (null proc) cur) + (and proc (eq proc cur)))))) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -585,12 +582,12 @@ Uses THING, FACE, DEFS and PREPEND." _code source message) diag-spec (eglot--with-lsp-range (beg end) range - (flymake-make-diagnostic (current-buffer) - beg end - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message)))) + (flymake-make-diagnostic (current-buffer) + beg end + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message)))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -657,18 +654,18 @@ Uses THING, FACE, DEFS and PREPEND." (append (eglot--VersionedTextDocumentIdentifier) (jrpc-obj :languageId - (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode)) - "unknown") - :text - (save-restriction - (widen) - (buffer-substring-no-properties (point-min) (point-max)))))) + (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode)) + "unknown") + :text + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." (jrpc-obj :textDocument (eglot--TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position))) + :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") @@ -931,7 +928,7 @@ DUMMY is ignored" (defun eglot--hover-info (contents &optional range) (concat (and range (eglot--with-lsp-range (beg end) range - (concat (buffer-substring beg end) ": "))) + (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append (cond ((vectorp contents) From f529f554a38c1d6cfbf2ece60d18a4d0c0a21caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 21:49:58 +0100 Subject: [PATCH 139/771] Jrpc-connect is now passed a generic dispatching function * eglot.el (eglot--dispatch): New helper. (eglot--connect): Use it. * jrpc.el (jrpc--dispatcher, jrpc--request-continuations) (jrpc--server-request-ids): New process-local var. (jrpc--pending-continuations, jrpc--method-prefix): Remove. (jrpc-connect): Take DISPATCHER instead of PREFIX. (jrpc--process-receive): Use proc's dispatcher. (jrpc--process-send): Make private. (jrpc-forget-pending-continuations, jrpc-async-request) (jrpc-reply, jrpc-notify): Use new function names. --- lisp/progmodes/eglot.el | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 879972df1b1..13aeff69569 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -277,9 +277,18 @@ INTERACTIVE is t if called interactively." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") +(defun eglot--dispatch (proc method id &rest params) + ;; a server notification or a server request + (let* ((handler-sym (intern (concat "eglot--server-" method)))) + (if (functionp handler-sym) + (apply handler-sym proc (append params (if id `(:id ,id)))) + (jrpc-reply + proc id + :error (jrpc-obj :code -32601 :message "Unimplemented"))))) + (defun eglot--connect (project managed-major-mode name command dont-inhibit) - (let ((proc (jrpc-connect name command "eglot--server-" #'eglot--on-shutdown))) + (let ((proc (jrpc-connect name command #'eglot--dispatch #'eglot--on-shutdown))) (setf (eglot--project proc) project) (setf (eglot--major-mode proc)managed-major-mode) (push proc (gethash project eglot--processes-by-project)) From 1deb7cf8acbe3280cb76578e2600d8e5de911aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 22:17:00 +0100 Subject: [PATCH 140/771] Fix a ridiculous bug when generating transient projects * eglot.el (eglot--find-current-process, eglot--interactive): Fix horrible bug. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13aeff69569..11d048e03aa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -136,7 +136,7 @@ called interactively." (defun eglot--find-current-process () "The current logical EGLOT process." - (let* ((probe (or (project-current) (cons 'transient default-directory)))) + (let* ((probe (or (project-current) `(transient . ,default-directory)))) (cl-find major-mode (gethash probe eglot--processes-by-project) :key #'eglot--major-mode))) @@ -208,7 +208,7 @@ called interactively." "\n" base-prompt))))) (list managed-mode - (or (project-current) `(transient . default-directory)) + (or (project-current) `(transient . ,default-directory)) (if prompt (split-string-and-unquote (read-shell-command prompt From 0d3e4ea1bdd9cf3e8d2368b897eb5ee6c07a3004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 22:19:23 +0100 Subject: [PATCH 141/771] Fix a ridiculous bug when generating transient projects * eglot.el (eglot--find-current-process, eglot--interactive): Fix horrible bug. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 85b2d89a67a..eb04c720b05 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -79,7 +79,7 @@ lasted more than that many seconds." (defun eglot--current-process () "The current logical EGLOT process." - (let* ((probe (or (project-current) (cons 'transient default-directory)))) + (let* ((probe (or (project-current) `(transient . ,default-directory)))) (cl-find major-mode (gethash probe eglot--processes-by-project) :key #'eglot--major-mode))) @@ -300,7 +300,7 @@ INTERACTIVE is t if inside interactive call." "\n" base-prompt))))) (list managed-mode - (or (project-current) `(transient . default-directory)) + (or (project-current) `(transient . ,default-directory)) (if prompt (split-string-and-unquote (read-shell-command prompt From 149bb814e642d367ffc2106ca1b1e88e90a38944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 23:09:27 +0100 Subject: [PATCH 142/771] Shutdown server if connection initialization fails Also tweak autoreconnection logic * eglot.el (eglot--connect): Immediately `eglot-shutdown` if connection initialization failed. Don't treat interactive calls specially. (eglot--process-sentinel): Tweak messages. --- lisp/progmodes/eglot.el | 50 +++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eb04c720b05..f87b4eaf124 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -224,7 +224,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--connect (project managed-major-mode short-name contact interactive) +(defun eglot--connect (project managed-major-mode short-name contact _interactive) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. INTERACTIVE is t if inside interactive call." (let* ((proc (eglot--make-process short-name managed-major-mode contact)) @@ -233,11 +233,10 @@ INTERACTIVE is t if inside interactive call." (eglot--project proc) project (eglot--major-mode proc) managed-major-mode) (with-current-buffer buffer - (let ((inhibit-read-only t)) + (let ((inhibit-read-only t) success) (setf (eglot--inhibit-autoreconnect proc) (cond ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) - (interactive nil) ((cl-plusp eglot-autoreconnect) (run-with-timer eglot-autoreconnect nil (lambda () @@ -248,24 +247,27 @@ INTERACTIVE is t if inside interactive call." (run-hook-with-args 'eglot-connect-hook proc) (erase-buffer) (read-only-mode t) - (cl-destructuring-bind (&key capabilities) - (eglot--request - proc - :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [] - :capabilities (eglot--client-capabilities))) - (setf (eglot--capabilities proc) capabilities) - (setf (eglot--status proc) nil) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))) - (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) - proc))))) + (unwind-protect + (cl-destructuring-bind (&key capabilities) + (eglot--request + proc + :initialize + (eglot--obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :capabilities(eglot--client-capabilities) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [])) + (setf (eglot--capabilities proc) capabilities) + (setf (eglot--status proc) nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode proc))) + (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) + (setq success proc)) + (unless (or success (not (process-live-p proc)) (eglot--moribund proc)) + (eglot-shutdown proc))))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -398,10 +400,10 @@ INTERACTIVE is t if called interactively." (eglot--message "Server exited with status %s" (process-exit-status proc)) (cond ((eglot--moribund proc)) ((not (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Reconnecting unexpected server exit.") + (eglot--warn "Reconnecting after unexpected server exit") (eglot-reconnect proc)) - (t - (eglot--warn "Not auto-reconnecting, last one didn't last long."))) + ((timerp (eglot--inhibit-autoreconnect proc)) + (eglot--warn "Not auto-reconnecting, last on didn't last long."))) (delete-process proc)))) (defun eglot--process-filter (proc string) From 11debd99a758647e0b04ba6f4417cdee4d31b631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 May 2018 23:58:29 +0100 Subject: [PATCH 143/771] More quietly report request timeouts as events * eglot.el (eglot--sync-request): Use eglot--log-event --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f87b4eaf124..83f4efd1437 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -612,9 +612,9 @@ timeout keeps counting." (remhash id (eglot--pending-continuations proc)) (funcall (or timeout-fn (lambda () - (eglot--error - "Tired of waiting for reply to %s, id=%s" - method id)))))))))) + (eglot--log-event + proc `(:timed-out ,method :id id + :params ,params))))))))))) (when deferred (let* ((buf (current-buffer)) (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) From 4ef2d1875cc2d1f8b7906ed0912176a4888436c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 15 May 2018 10:59:46 +0100 Subject: [PATCH 144/771] Bump version and slightly improve doc * eglot.el: Bump version. Add nicer Commentary header. (eglot): Improve docstring. * README.md: Update --- lisp/progmodes/eglot.el | 51 +++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 83f4efd1437..4ff6204fc39 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.1 +;; Version: 0.2 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot @@ -24,8 +24,28 @@ ;;; Commentary: -;; Simply M-x eglot should be enough to get you started, but see README.md. - +;; Simply M-x eglot should be enough to get you started, but here's a +;; little info (see the accompanying README.md or the URL for more). +;; +;; M-x eglot starts a server via a shell-command guessed from +;; `eglot-server-programs', using the current major-mode (for whatever +;; language you're programming in) as a hint. If it can't guess, it +;; prompts you in the mini-buffer for these things. Actually, the +;; server needen't be locally started: you can connect to a running +;; server via TCP by entering a syntax. +;; +;; Anyway, if the connection is successful, you should see an `eglot' +;; indicator pop up in your mode-line. More importantly, this means +;; current *and future* file buffers of that major mode *inside your +;; current project* automatically become \"managed\" by the LSP +;; server, i.e. information about their contents is exchanged +;; periodically to provide enhanced code analysis via +;; `xref-find-definitions', `flymake-mode', `eldoc-mode', +;; `completion-at-point', among others. +;; +;; To "unmanage" these buffers, shutdown the server with M-x +;; eglot-shutdown. +;; ;;; Code: (require 'json) @@ -314,9 +334,22 @@ INTERACTIVE is t if inside interactive call." ;;;###autoload (defun eglot (managed-major-mode project command &optional interactive) - "Start a Language Server Protocol server. -Server is started with COMMAND and manages buffers of -MANAGED-MAJOR-MODE for the current project. + "Manage a project with a Language Server Protocol (LSP) server. + +The LSP server is started (or contacted) via COMMAND. If this +operation is successful, current *and future* file buffers of +MANAGED-MAJOR-MODE inside PROJECT automatically become +\"managed\" by the LSP server, meaning information about their +contents is exchanged periodically to provide enhanced +code-analysis via `xref-find-definitions', `flymake-mode', +`eldoc-mode', `completion-at-point', among others. + +Interactively, the command attempts to guess MANAGED-MAJOR-MODE +from current buffer, COMMAND from `eglot-server-programs' and +PROJECT from `project-current'. If it can't guess, the user is +prompted. With a single \\[universal-argument] prefix arg, it +always prompt for COMMAND. With two \\[universal-argument] +prefix args, also prompts for MANAGED-MAJOR-MODE. PROJECT is a project instance as returned by `project-current'. @@ -328,12 +361,6 @@ is also know as the server's \"contact\". MANAGED-MAJOR-MODE is an Emacs major mode. -Interactively, guess MANAGED-MAJOR-MODE from current buffer and -COMMAND from `eglot-server-programs'. With a single -\\[universal-argument] prefix arg, prompt for COMMAND. With two -\\[universal-argument] prefix args, also prompt for -MANAGED-MAJOR-MODE. - INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) (let* ((short-name (eglot--project-short-name project))) From 40e256a1bf6d237eb5637fd726077332519338a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 15 May 2018 13:24:08 +0100 Subject: [PATCH 145/771] Add php's php-language-server to built-in guessed servers Closes https://github.com/joaotavora/eglot/issues/1. The problem in that issue is that php-language-server has a bug when it's not passed it the deprecated ":rootPath" field. The bug doesn't happen if the ":processId" field is also absent. Eglot was triggering the bug, because it didn't pass ":rootPath", but there's nothing wrong in doing so. * README.md: Add php-language-server to the built-in list. * eglot.el (eglot-server-programs): Add php-language-server. (eglot--connect): Also pass (deprecated) rootPath. --- lisp/progmodes/eglot.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4ff6204fc39..da3a09f3b22 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -71,7 +71,9 @@ (defvar eglot-server-programs '((rust-mode . ("rls")) (python-mode . ("pyls")) (js-mode . ("javascript-typescript-stdio")) - (sh-mode . ("bash-language-server" "start"))) + (sh-mode . ("bash-language-server" "start")) + (php-mode . ("php" "vendor/felixfbecker/\ +language-server/bin/php-language-server.php"))) "Alist mapping major modes to server executables.") (defface eglot-mode-line @@ -276,6 +278,7 @@ INTERACTIVE is t if inside interactive call." 'network) (emacs-pid)) :capabilities(eglot--client-capabilities) + :rootPath (car (project-roots project)) :rootUri (eglot--path-to-uri (car (project-roots project))) :initializationOptions [])) From 8bbd2ba28d0774c3d2547c35e9d34d184d90bdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 May 2018 14:21:27 +0100 Subject: [PATCH 146/771] More flexible jrpc.el and improve eglot.el's doc Generalize and rework CONTACT arg to jrpc-connect * eglot.el (eglot--command-history): Tweak docstring. (eglot--interactive): Rework. (eglot): Rework docstring. COMMAND is now CONTACT. (eglot--connect): Use new jrpc-connect protocol. (eglot-server-programs): Reword doc. * jrpc.el (jrpc--make-process): Use new form of CONTACT. (jrpc-connect): Explain new semantics of CONTACT. --- lisp/progmodes/eglot.el | 66 ++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13c1b49c758..4f2e25b2899 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -75,7 +75,9 @@ (sh-mode . ("bash-language-server" "start")) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) - "Alist mapping major modes to server executables.") + "Alist of (MAJOR-MODE . CONTACT) mapping major modes to server executables. +CONTACT can be anything accepted by that parameter in the +function `eglot', which see.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -198,7 +200,7 @@ called interactively." :experimental (jrpc-obj))) (defvar eglot--command-history nil - "History of COMMAND arguments to `eglot'.") + "History of CONTACT arguments to `eglot'.") (defun eglot--interactive () "Helper for `eglot'." @@ -213,6 +215,7 @@ called interactively." (mapcar #'symbol-name (eglot--all-major-modes)) nil t (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) (t guessed-mode))) + (project (or (project-current) `(transient . ,default-directory))) (guessed-command (cdr (assoc managed-mode eglot-server-programs))) (base-prompt "[eglot] Enter program to execute (or :): ") (prompt @@ -222,26 +225,30 @@ called interactively." managed-mode) "\n" base-prompt)) ((and (listp guessed-command) + (not (integerp (cadr guessed-command))) (not (executable-find (car guessed-command)))) (concat (format "[eglot] I guess you want to run `%s'" (combine-and-quote-strings guessed-command)) (format ", but I can't find `%s' in PATH!" (car guessed-command)) - "\n" base-prompt))))) - (list - managed-mode - (or (project-current) `(transient . ,default-directory)) - (if prompt - (split-string-and-unquote - (read-shell-command prompt - (if (listp guessed-command) - (combine-and-quote-strings guessed-command)) - 'eglot-command-history)) - guessed-command) - t))) + "\n" base-prompt)))) + (contact + (cond ((not prompt) guessed-command) + (t + (let ((string (read-shell-command + prompt + (if (listp guessed-command) + (combine-and-quote-strings guessed-command)) + 'eglot-command-history))) + (if (and string (string-match + "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" + (string-trim string))) + (list (match-string 1 string) (match-string 2 string)) + (split-string-and-unquote string))))))) + (list managed-mode project contact t))) ;;;###autoload -(defun eglot (managed-major-mode project command &optional interactive) +(defun eglot (managed-major-mode project contact &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. The LSP server is started (or contacted) via COMMAND. If this @@ -253,7 +260,7 @@ code-analysis via `xref-find-definitions', `flymake-mode', `eldoc-mode', `completion-at-point', among others. Interactively, the command attempts to guess MANAGED-MAJOR-MODE -from current buffer, COMMAND from `eglot-server-programs' and +from current buffer, CONTACT from `eglot-server-programs' and PROJECT from `project-current'. If it can't guess, the user is prompted. With a single \\[universal-argument] prefix arg, it always prompt for COMMAND. With two \\[universal-argument] @@ -261,11 +268,14 @@ prefix args, also prompts for MANAGED-MAJOR-MODE. PROJECT is a project instance as returned by `project-current'. -COMMAND is a list of strings, an executable program and -optionally its arguments. If the first and only string in the -list is of the form \":\" it is taken as an -indication to connect to a server instead of starting one. This -is also know as the server's \"contact\". +CONTACT is a list of strings (COMMAND [ARGS...]) specifying how +to start a server subprocess to connect to. If the second +element in the list is an integer number instead of a string, the +list is interpreted as (HOST PORT [PARAMETERS...]) to connect to +an existing server via TCP, the remaining PARAMETERS being given +as `open-network-stream's optional arguments. CONTACT can also +be a function of no arguments returning a live connected process +object. MANAGED-MAJOR-MODE is an Emacs major mode. @@ -282,7 +292,7 @@ INTERACTIVE is t if called interactively." (let ((proc (eglot--connect project managed-major-mode (format "%s/%s" short-name managed-major-mode) - command))) + contact))) (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." proc managed-major-mode short-name) @@ -310,13 +320,13 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (let* ((handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) (apply handler-sym proc (append params (if id `(:id ,id)))) - (jrpc-reply - proc id + (jrpc-reply proc id :error (jrpc-obj :code -32601 :message "Unimplemented"))))) -(defun eglot--connect (project managed-major-mode name command) - (let ((proc (jrpc-connect name command #'eglot--dispatch #'eglot--on-shutdown)) - success) +(defun eglot--connect (project managed-major-mode name contact) + (let* ((contact (if (functionp contact) (funcall contact) contact)) + (proc (jrpc-connect name contact #'eglot--dispatch #'eglot--on-shutdown)) + success) (setf (eglot--project proc) project) (setf (eglot--major-mode proc)managed-major-mode) (push proc (gethash project eglot--processes-by-project)) @@ -350,7 +360,7 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (null eglot-autoreconnect))))))) (setq success proc)) (unless (or success (not (process-live-p proc)) (eglot--moribund proc)) - (eglot-shutdown proc))))) + (eglot-shutdown proc))))) (defun eglot--server-ready-p (_what _proc) "Tell if server of PROC ready for processing deferred WHAT." From a6046e9efd71e0efabe487686a4f8c08de76d78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 May 2018 23:39:12 +0100 Subject: [PATCH 147/771] Simpler callback protocol for jsonrpc parameters and results Instead of introspecting the :params or :result object to discover if an object is present, and changing the Elisp function call type (funcall vs apply) accordingly, alway funcall. It's up to the application to destructure if it wishes. jrpc-lambda can help with that and keep the application code simple. * eglot.el (eglot--on-shutdown): Fix indentation. (eglot--dispatch): Simplify. (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point) (eglot-eldoc-function, eglot-imenu, eglot--apply-text-edits): Don't use jrpc-mapply. * jrpc.el (jrpc--process-receive): Allow only keys defined in JSONRPC2.0 (jrpc--process-receive): Don't overload function call type based on remote response. (jrpc-lambda): Return a unary lambda. (jrpc-request): Simplify. (jrpc-mapply): Remove. --- lisp/progmodes/eglot.el | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4f2e25b2899..caf2e8c82fb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -128,7 +128,7 @@ A list (ID WHAT DONE-P).") (eglot--managed-mode -1)))) ;; Kill any expensive watches (maphash (lambda (_id watches) - (mapcar #'file-notify-rm-watch watches)) + (mapcar #'file-notify-rm-watch watches)) (eglot--file-watches proc)) ;; Sever the project/process relationship for proc (setf (gethash (eglot--project proc) eglot--processes-by-project) @@ -314,7 +314,7 @@ INTERACTIVE is t if called interactively." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--dispatch (proc method id &rest params) +(defun eglot--dispatch (proc method id params) "Dispatcher passed to `jrpc-connect'. Builds a function from METHOD, passes it PROC, ID and PARAMS." (let* ((handler-sym (intern (concat "eglot--server-" method)))) @@ -865,7 +865,7 @@ DUMMY is ignored" (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols - (jrpc-mapply + (mapcar (jrpc-lambda (&key name kind location containerName) (propertize name :textDocumentPositionParams @@ -898,7 +898,7 @@ DUMMY is ignored" :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) - (jrpc-mapply + (mapcar (jrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) location-or-locations))) @@ -912,7 +912,7 @@ DUMMY is ignored" (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (jrpc-mapply + (mapcar (jrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) (jrpc-request (jrpc-current-process-or-lose) @@ -924,7 +924,7 @@ DUMMY is ignored" (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (jrpc-mapply + (mapcar (jrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) @@ -947,7 +947,7 @@ DUMMY is ignored" (eglot--TextDocumentPositionParams) :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (jrpc-mapply + (mapcar (jrpc-lambda (&rest all &key label &allow-other-keys) (add-text-properties 0 1 all label) label) items)))) @@ -1040,7 +1040,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (jrpc-async-request proc :textDocument/signatureHelp position-params :success-fn (jrpc-lambda (&key signatures activeSignature - activeParameter) + activeParameter) (when-buffer-window (when (cl-plusp (length signatures)) (setq sig-showing t) @@ -1063,7 +1063,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights (when-buffer-window - (jrpc-mapply + (mapcar (jrpc-lambda (&key range _kind) (eglot--with-lsp-range (beg end) range (let ((ov (make-overlay beg end))) @@ -1078,7 +1078,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "EGLOT's `imenu-create-index-function' overriding OLDFUN." (if (eglot--server-capable :documentSymbolProvider) (let ((entries - (jrpc-mapply + (mapcar (jrpc-lambda (&key name kind location _containerName) (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point @@ -1098,14 +1098,13 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (jrpc-mapply - (jrpc-lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (eglot--with-lsp-range (beg end) range - (goto-char beg) (delete-region beg end) (insert newText))))) - edits) + (mapc (jrpc-lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (eglot--with-lsp-range (beg end) range + (goto-char beg) (delete-region beg end) (insert newText))))) + edits) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) From 5db50ddd47518aa82e0be1584e7019f6ae60adbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 May 2018 23:45:35 +0100 Subject: [PATCH 148/771] Replace eglot--with-lsp-range with a function and pcase-let * eglot.el (eglot--with-lsp-range): Remove. (eglot--range-region): New function. (eglot--server-textDocument/publishDiagnostics) (eglot--hover-info, eglot-eldoc-function) (eglot--apply-text-edits): Use it. --- lisp/progmodes/eglot.el | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index da3a09f3b22..0c11b96cbe5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -805,14 +805,10 @@ DEFERRED is passed to `eglot--async-request', which see." "Determine if current server is capable of FEAT." (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) -(cl-defmacro eglot--with-lsp-range ((start end) range &body body - &aux (range-sym (cl-gensym))) - "Bind LSP RANGE to START and END. Evaluate BODY." - (declare (indent 2) (debug (sexp sexp &rest form))) - `(let* ((,range-sym ,range) - (,start (eglot--lsp-position-to-point (plist-get ,range-sym :start))) - (,end (eglot--lsp-position-to-point (plist-get ,range-sym :end)))) - ,@body)) +(defun eglot--range-region (range) + "Return region (BEG . END) that represents LSP RANGE." + (cons (eglot--lsp-position-to-point (plist-get range :start)) + (eglot--lsp-position-to-point (plist-get range :end)))) ;;; Minor modes @@ -1016,7 +1012,7 @@ called interactively." collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (eglot--with-lsp-range (beg end) range + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (flymake-make-diagnostic (current-buffer) beg end (cond ((<= severity 1) :error) @@ -1355,7 +1351,7 @@ DUMMY is ignored" (defun eglot--hover-info (contents &optional range) (concat (and range - (eglot--with-lsp-range (beg end) range + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append @@ -1436,7 +1432,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-buffer-window (eglot--mapply (eglot--lambda (&key range _kind) - (eglot--with-lsp-range (beg end) 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) @@ -1474,7 +1471,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (save-restriction (widen) (save-excursion - (eglot--with-lsp-range (beg end) range + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (goto-char beg) (delete-region beg end) (insert newText))))) edits) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) From 0eb1ef8d36124a63d211c7d7f7c16ba17cbed975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 17 May 2018 00:30:53 +0100 Subject: [PATCH 149/771] Simplify some function calling infrastructure eglot--mapply is a confusing abstraction. Hide some of that confusion behind eglot--lambda. More stably dispatch server notifications and requests without introspecting their contents. * eglot.el (eglot--process-receive): Simplify. (eglot--async-request): Improve doc. (eglot--request): Simplify. (eglot--mapply): Remove. (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point) (eglot-eldoc-function, eglot-imenu, eglot--apply-text-edits): Don't use eglot--mapply, use normal mapcar/mapc. --- lisp/progmodes/eglot.el | 145 ++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 81 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0c11b96cbe5..e17e4f8766e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -541,7 +541,7 @@ is a symbol saying if this is a client or server originated." (defun eglot--process-receive (proc message) "Process MESSAGE from PROC." - (cl-destructuring-bind (&key method id error &allow-other-keys) message + (cl-destructuring-bind (&key method id params error result _jsonrpc) message (let* ((continuations (and id (not method) (gethash id (eglot--pending-continuations proc))))) @@ -551,24 +551,19 @@ is a symbol saying if this is a client or server originated." ;; a server notification or a server request (let* ((handler-sym (intern (concat "eglot--server-" method)))) (if (functionp handler-sym) - (apply handler-sym proc (append - (plist-get message :params) - (if id `(:id ,id)))) + ;; FIXME: will fail if params is array instead of not an object + (apply handler-sym proc (append params (if id `(:id ,id)))) (eglot--warn "No implementation of method %s yet" method) (when id (eglot--reply proc id - :error (eglot--obj :code -32601 - :message "Method unimplemented")))))) + :error `(:code -32601 :message "Method unimplemented")))))) (continuations (cancel-timer (cl-third continuations)) (remhash id (eglot--pending-continuations proc)) (if error - (apply (cl-second continuations) error) - (let ((res (plist-get message :result))) - (if (listp res) - (apply (cl-first continuations) res) - (funcall (cl-first continuations) res))))) + (funcall (cl-second continuations) error) + (funcall (cl-first continuations) result))) (id (eglot--warn "Ooops no continuation for id %s" id))) (eglot--call-deferred proc) @@ -615,8 +610,9 @@ request request and a process object.") (not (eglot--outstanding-edits-p))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1) (debug (sexp &rest form))) - `(cl-function (lambda ,cl-lambda-list ,@body))) + (declare (debug (sexp &rest form))) + (let ((e (gensym "eglot--lambda-elem"))) + `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) (cl-defun eglot--async-request (proc method @@ -625,12 +621,14 @@ request request and a process object.") &key success-fn error-fn timeout-fn (timeout eglot-request-timeout) (deferred nil)) - "Make a request to PROCESS, expecting a reply. -Return the ID of this request. Wait TIMEOUT seconds for response. -If DEFERRED, maybe defer request to the future, or never at all, -in case a new request with identical DEFERRED and for the same -buffer overrides it. However, if that happens, the original -timeout keeps counting." + "Make a request to PROCESS, expecting a reply later on. +SUCCESS-FN and ERROR-FN are passed `:result' and `:error' +objects, respectively. Wait TIMEOUT seconds for response or call +nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the +future, or to never at all, in case a new request with identical +DEFERRED and for the same buffer overrides it (however, if that +happens, the original timeout keeps counting). Return the ID of +this request." (let* ((id (eglot--next-request-id)) (existing-timer nil) (make-timeout @@ -692,23 +690,18 @@ DEFERRED is passed to `eglot--async-request', which see." (when deferred (eglot--signal-textDocument/didChange)) (let* ((done (make-symbol "eglot--request-catch-tag")) (res - (catch done (eglot--async-request - proc method params - :success-fn (lambda (&rest args) - (throw done (if (vectorp (car args)) - (car args) args))) - :error-fn (eglot--lambda - (&key code message &allow-other-keys) - (throw done - `(error ,(format "Oops: %s: %s" - code message)))) - :timeout-fn (lambda () - (throw done '(error "Timed out"))) - :deferred deferred) - ;; now spin, baby! - (while t (accept-process-output nil 0.01))))) - (when (and (listp res) (eq 'error (car res))) (eglot--error (cadr res))) - res)) + (catch done + (eglot--async-request + proc method params + :success-fn (lambda (result) (throw done `(done ,result))) + :timeout-fn (lambda () (throw done '(error "Timed out"))) + :error-fn (eglot--lambda (&key code message _data) + (throw done `(error + ,(format "Ooops: %s: %s" code message)))) + :deferred deferred) + (while t (accept-process-output nil 30))))) + (when (eq 'error (car res)) (eglot--error (cadr res))) + (cadr res))) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" @@ -762,11 +755,6 @@ DEFERRED is passed to `eglot--async-request', which see." (line-beginning-position)))) (point))) - -(defun eglot--mapply (fun seq) - "Apply FUN to every element of SEQ." - (mapcar (lambda (e) (apply fun e)) seq)) - (defun eglot--path-to-uri (path) "Urify PATH." (url-hexify-string (concat "file://" (file-truename path)) @@ -1232,7 +1220,7 @@ DUMMY is ignored" (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols - (eglot--mapply + (mapcar (eglot--lambda (&key name kind location containerName) (propertize name :textDocumentPositionParams @@ -1265,10 +1253,9 @@ DUMMY is ignored" :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) - (eglot--mapply - (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - location-or-locations))) + (mapcar (eglot--lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) @@ -1279,25 +1266,23 @@ DUMMY is ignored" (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--mapply - (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - (eglot--request (eglot--current-process-or-lose) - :textDocument/references - (append - params - (eglot--obj :context - (eglot--obj :includeDeclaration t))))))) + (mapcar (eglot--lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--request (eglot--current-process-or-lose) + :textDocument/references + (append + params + (eglot--obj :context + (eglot--obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (eglot--mapply - (eglot--lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) - (eglot--request (eglot--current-process-or-lose) - :workspace/symbol - (eglot--obj :query pattern))))) + (mapcar (eglot--lambda (&key name location &allow-other-keys) + (cl-destructuring-bind (&key uri range) location + (eglot--xref-make name uri (plist-get range :start)))) + (eglot--request (eglot--current-process-or-lose) + :workspace/symbol + (eglot--obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." @@ -1314,7 +1299,7 @@ DUMMY is ignored" (eglot--TextDocumentPositionParams) :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (eglot--mapply + (mapcar (eglot--lambda (&rest all &key label &allow-other-keys) (add-text-properties 0 1 all label) label) items)))) @@ -1430,15 +1415,14 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights (when-buffer-window - (eglot--mapply - (eglot--lambda (&key range _kind) - (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)))) + (mapcar (eglot--lambda (&key range _kind) + (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) @@ -1446,7 +1430,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "EGLOT's `imenu-create-index-function' overriding OLDFUN." (if (eglot--server-capable :documentSymbolProvider) (let ((entries - (eglot--mapply + (mapcar (eglot--lambda (&key name kind location _containerName) (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point @@ -1466,14 +1450,13 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (eglot--mapply - (eglot--lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (goto-char beg) (delete-region beg end) (insert newText))))) - edits) + (mapc (eglot--lambda (&key range newText) + (save-restriction + (widen) + (save-excursion + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (goto-char beg) (delete-region beg end) (insert newText))))) + edits) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) From b7d0c91afc3d954d1d47e4dce48d607b87bdfcf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 17 May 2018 14:04:33 +0100 Subject: [PATCH 150/771] * eglot.el (eglot--lambda): add missing indent spec. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e17e4f8766e..30d5b435bbf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -610,7 +610,7 @@ request request and a process object.") (not (eglot--outstanding-edits-p))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (debug (sexp &rest form))) + (declare (indent 1) (debug (sexp &rest form))) (let ((e (gensym "eglot--lambda-elem"))) `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) From 1104060048f1e2fae7e5ed19a00bbe1d738a5861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 17 May 2018 14:04:15 +0100 Subject: [PATCH 151/771] Fix eglot--error and eglot--message helpers * eglot.el (eglot--error, eglot--message): Safely interpret %s. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 30d5b435bbf..01a6b5d47f1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -721,11 +721,11 @@ DEFERRED is passed to `eglot--async-request', which see." ;;; (defun eglot--error (format &rest args) "Error out with FORMAT with ARGS." - (error (apply #'format format args))) + (error "[eglot] %s" (apply #'format format args))) (defun eglot--message (format &rest args) "Message out with FORMAT with ARGS." - (message (concat "[eglot] " (apply #'format format args)))) + (message "[eglot] %s" (apply #'format format args))) (defun eglot--warn (format &rest args) "Warning message with FORMAT and ARGS." From 04da3b6abdcddebdaf899a1f2f0de511f4174146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 17 May 2018 14:03:20 +0100 Subject: [PATCH 152/771] Make it work on windows Apparently passing :coding 'no-conversion to make-process on windows is essential to receive any text at all in the process filter. Also needed to tweak uri-to-path and path-to-uri. Thanks to lsp-mode.el for these hints * eglot.el (eglot--make-process): Pass :coding 'no-conversion to make-process. (eglot--path-to-uri): Add a forward slash if windows-nt. (eglot--uri-to-path): Remove a forward slash if windows-nt. (eglot--server-textDocument/publishDiagnostics): Simplify and use eglot--uri-to-path. --- lisp/progmodes/eglot.el | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 01a6b5d47f1..08c2f55dbe7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -195,6 +195,7 @@ CONTACT is as `eglot--contact'. Returns a process object." (make-process :name readable-name :buffer buffer :command contact + :coding 'no-conversion :connection-type 'pipe :stderr (get-buffer-create (format "*%s stderr*" name)))))) @@ -756,14 +757,16 @@ DEFERRED is passed to `eglot--async-request', which see." (point))) (defun eglot--path-to-uri (path) - "Urify PATH." - (url-hexify-string (concat "file://" (file-truename path)) - url-path-allowed-chars)) + "URIfy PATH." + (url-hexify-string + (concat "file://" (if (eq system-type 'windows-nt) "/") (file-truename path)) + url-path-allowed-chars)) (defun eglot--uri-to-path (uri) "Convert URI to a file path." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) - (url-filename (url-generic-parse-url (url-unhex-string uri)))) + (let ((retval (url-filename (url-generic-parse-url (url-unhex-string uri))))) + (if (eq system-type 'windows-nt) (substring retval 1) retval))) (defconst eglot--kind-names `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") @@ -989,11 +992,7 @@ called interactively." (cl-defun eglot--server-textDocument/publishDiagnostics (_process &key uri diagnostics) "Handle notification publishDiagnostics" - (let* ((obj (url-generic-parse-url uri)) - (filename (car (url-path-and-query obj))) - (buffer (find-buffer-visiting filename))) - (cond - (buffer + (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer (cl-loop for diag-spec across diagnostics @@ -1012,9 +1011,8 @@ called interactively." (funcall eglot--current-flymake-report-fn diags) (setq eglot--unreported-diagnostics nil)) (t - (setq eglot--unreported-diagnostics diags)))))) - (t - (eglot--message "OK so %s isn't visited" filename))))) + (setq eglot--unreported-diagnostics diags))))) + (eglot--warn "Diagnostics received for unvisited %s" uri))) (cl-defun eglot--register-unregister (proc jsonrpc-id things how) "Helper for `eglot--server-client/registerCapability'. From 35962402594c6aaf320878e1c5a6433f1d0553d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 17 May 2018 14:09:50 +0100 Subject: [PATCH 153/771] * eglot.el (version): bump to 0.3 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 08c2f55dbe7..4a847bb6029 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.2 +;; Version: 0.3 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From 868d531c9ee06cf2ebd7f4232c148f8e414f98ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 18 May 2018 12:35:36 +0100 Subject: [PATCH 154/771] Improve jrpc.el's doc (and change jrpc-request's protocol a tiny bit) * jrpc.el (jrpc-async-request) (jrpc-request,jrpc-notify,jrpc-reply): Improve docstring. (jrpc-connect): Improve docstring and add autoload cookie (jrpc-request): DEFERRED param is now &key (defgroup jrpc): Fix description. * eglot.el (advice-add jrpc-request): Use &key deferred. (eglot-completion-at-point): Pass :deferred to jrpc-request --- lisp/progmodes/eglot.el | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b820ddeae5c..4e02c72e3e7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -744,11 +744,13 @@ Records START, END and PRE-CHANGE-LENGTH locally." `[(,pre-change-length ,(buffer-substring-no-properties start end))]))) -;; HACK! +;; HACK! Launching a deferred sync request with outstanding changes is a +;; bad idea, since that might lead to the request never having a +;; chance to run, because `jrpc-ready-predicates'. (advice-add #'jrpc-request :before - (lambda (_proc _method _params &optional deferred) + (cl-function (lambda (_proc _method _params &key deferred) (when (and eglot--managed-mode deferred) - (eglot--signal-textDocument/didChange)))) + (eglot--signal-textDocument/didChange))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." @@ -937,7 +939,7 @@ DUMMY is ignored" (let* ((resp (jrpc-request proc :textDocument/completion (eglot--TextDocumentPositionParams) - :textDocument/completion)) + :deferred :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jrpc-lambda (&rest all &key label &allow-other-keys) From 99cb423db699ec1ed359326b9b6f9892067de5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 18 May 2018 16:52:19 +0100 Subject: [PATCH 155/771] Jrpc.el should know nothing of mode-line updates * eglot.el (eglot--dispatch): METHOD can be a symbol. Call force-mode-line-update here. --- lisp/progmodes/eglot.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4e02c72e3e7..81229a58177 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -317,11 +317,12 @@ INTERACTIVE is t if called interactively." (defun eglot--dispatch (proc method id params) "Dispatcher passed to `jrpc-connect'. Builds a function from METHOD, passes it PROC, ID and PARAMS." - (let* ((handler-sym (intern (concat "eglot--server-" method)))) + (let* ((handler-sym (intern (format "eglot--server-%s" method)))) (if (functionp handler-sym) ;; FIXME: fails if params is array, not object (apply handler-sym proc (append params (if id `(:id ,id)))) (jrpc-reply proc id - :error (jrpc-obj :code -32601 :message "Unimplemented"))))) + :error (jrpc-obj :code -32601 :message "Unimplemented"))) + (force-mode-line-update t))) (defun eglot--connect (project managed-major-mode name contact) (let* ((contact (if (functionp contact) (funcall contact) contact)) From 2290ce100fc56560c63cbd0d50816e725023315a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 09:29:52 +0100 Subject: [PATCH 156/771] Simplify some infrastructure fucntions * eglot.el (eglot--contact): Simplify docstring. (eglot--make-process): Simplify. (eglot--connect): Simplify. (eglot--interactive): Simplify and correct odd bug. (eglot--process-sentinel): Correct messages. Delete before attempting reconnection. (eglot-shutdown): Simplify. --- lisp/progmodes/eglot.el | 113 +++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 60 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4a847bb6029..0a2fcf5955c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -163,9 +163,7 @@ A list (WHAT SERIOUS-P).") "If non-nil, don't autoreconnect on unexpected quit.") (eglot--define-process-var eglot--contact nil - "Method used to contact a server. -Either a list of strings (a shell command and arguments), or a -list of a single string of the form :") + "Method used to contact a server.") (eglot--define-process-var eglot--deferred-actions (make-hash-table :test #'equal) @@ -176,29 +174,23 @@ list of a single string of the form :") (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. -NAME is a name to give the inferior process or connection. +NAME is used to name the the started process or connection. MANAGED-MAJOR-MODE is a symbol naming a major mode. -CONTACT is as `eglot--contact'. Returns a process object." +CONTACT is in `eglot'. Returns a process object." (let* ((readable-name (format "EGLOT server (%s/%s)" name managed-major-mode)) - (buffer (get-buffer-create - (format "*%s inferior*" readable-name))) - singleton - (proc - (if (and (setq singleton (and (null (cdr contact)) (car contact))) - (string-match "^[\s\t]*\\(.*\\):\\([[:digit:]]+\\)[\s\t]*$" - singleton)) - (open-network-stream readable-name - buffer - (match-string 1 singleton) - (string-to-number - (match-string 2 singleton))) - (make-process :name readable-name - :buffer buffer - :command contact - :coding 'no-conversion - :connection-type 'pipe - :stderr (get-buffer-create (format "*%s stderr*" - name)))))) + (buffer (get-buffer-create (format "*%s stdout*" readable-name))) + (proc (cond + ((processp contact) contact) + ((integerp (cadr contact)) + (apply #'open-network-stream readable-name buffer contact)) + (t (make-process + :name readable-name + :command contact + :coding 'no-conversion + :connection-type 'pipe + :stderr (get-buffer-create (format "*%s stderr*" name))))))) + (set-process-buffer proc buffer) + (set-marker (process-mark proc) (with-current-buffer buffer (point-min))) (set-process-filter proc #'eglot--process-filter) (set-process-sentinel proc #'eglot--process-sentinel) proc)) @@ -250,7 +242,9 @@ CONTACT is as `eglot--contact'. Returns a process object." (defun eglot--connect (project managed-major-mode short-name contact _interactive) "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. INTERACTIVE is t if inside interactive call." - (let* ((proc (eglot--make-process short-name managed-major-mode contact)) + (let* ((proc (eglot--make-process + short-name managed-major-mode (if (functionp contact) + (funcall contact) contact))) (buffer (process-buffer proc))) (setf (eglot--contact proc) contact (eglot--project proc) project @@ -309,32 +303,32 @@ INTERACTIVE is t if inside interactive call." (mapcar #'symbol-name (eglot--all-major-modes)) nil t (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) (t guessed-mode))) - (guessed-command (cdr (assoc managed-mode eglot-server-programs))) + (project (or (project-current) `(transient . ,default-directory))) + (guessed (cdr (assoc managed-mode eglot-server-programs))) + (program (and (listp guessed) (stringp (car guessed)) (car guessed))) (base-prompt "[eglot] Enter program to execute (or :): ") (prompt (cond (current-prefix-arg base-prompt) - ((null guessed-command) - (concat (format "[eglot] Sorry, couldn't guess for `%s'!" - managed-mode) - "\n" base-prompt)) - ((and (listp guessed-command) - (not (executable-find (car guessed-command)))) + ((null guessed) + (format "[eglot] Sorry, couldn't guess for `%s'\n%s!" + managed-mode base-prompt)) + ((and program (not (executable-find program))) (concat (format "[eglot] I guess you want to run `%s'" - (combine-and-quote-strings guessed-command)) - (format ", but I can't find `%s' in PATH!" - (car guessed-command)) - "\n" base-prompt))))) - (list - managed-mode - (or (project-current) `(transient . ,default-directory)) - (if prompt - (split-string-and-unquote - (read-shell-command prompt - (if (listp guessed-command) - (combine-and-quote-strings guessed-command)) - 'eglot-command-history)) - guessed-command) - t))) + (combine-and-quote-strings guessed)) + (format ", but I can't find `%s' in PATH!" program) + "\n" base-prompt)))) + (contact + (if prompt + (let ((s (read-shell-command + prompt + (if program (combine-and-quote-strings guessed)) + 'eglot-command-history))) + (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" + (string-trim s)) + (list (match-string 1 s) (string-to-number (match-string 2 s))) + (split-string-and-unquote s))) + guessed))) + (list managed-mode project contact t))) ;;;###autoload (defun eglot (managed-major-mode project command &optional interactive) @@ -417,7 +411,7 @@ INTERACTIVE is t if called interactively." ;; Call all outstanding error handlers (maphash (lambda (_id triplet) (cl-destructuring-bind (_success error _timeout) triplet - (funcall error :code -1 :message (format "Server died")))) + (funcall error `(:code -1 :message "Server died")))) (eglot--pending-continuations proc)) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (buffer-list)) @@ -428,14 +422,16 @@ INTERACTIVE is t if called interactively." (setf (gethash (eglot--project proc) eglot--processes-by-project) (delq proc (gethash (eglot--project proc) eglot--processes-by-project))) - (eglot--message "Server exited with status %s" (process-exit-status proc)) + ;; Say last words + (eglot--message "%s exited with status %s" proc (process-exit-status proc)) + (delete-process proc) + ;; Consider autoreconnecting (cond ((eglot--moribund proc)) ((not (eglot--inhibit-autoreconnect proc)) (eglot--warn "Reconnecting after unexpected server exit") (eglot-reconnect proc)) ((timerp (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Not auto-reconnecting, last on didn't last long."))) - (delete-process proc)))) + (eglot--warn "Not auto-reconnecting, last on didn't last long.")))))) (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." @@ -934,23 +930,20 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (proc &optional interactive) +(defun eglot-shutdown (proc &optional _interactive) "Politely ask the server PROC to quit. Forcefully quit it if it doesn't respond. Don't leave this -function with the server still running. INTERACTIVE is t if -called interactively." +function with the server still running." (interactive (list (eglot--current-process-or-lose) t)) - (when interactive (eglot--message "Asking %s politely to terminate" proc)) + (eglot--message "Asking %s politely to terminate" proc) (unwind-protect (let ((eglot-request-timeout 3)) (setf (eglot--moribund proc) t) - (eglot--request proc - :shutdown - nil) - ;; this one should always fail + (eglot--request proc :shutdown nil) + ;; this one is supposed to always fail, hence ignore-errors (ignore-errors (eglot--request proc :exit nil))) (when (process-live-p proc) - (eglot--warn "Brutally deleting existing process %s" proc) + (eglot--warn "Brutally deleting non-compliant existing process %s" proc) (delete-process proc)))) (cl-defun eglot--server-window/showMessage (_process &key type message) From 6ee1deebf74ec41b14175ded65c3e9e92734b09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 10:06:12 +0100 Subject: [PATCH 157/771] Robustify timer handling for eglot--async-request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This basically cherry-picks an ealier commit for the jsonrpc-refactor branch: a2aa1ed..: João Távora 2018-05-18 Robustify timer handling for jrpc-async-request * jrpc.el (jrpc--async-request): Improve timeout handling. Return a list (ID TIMER) (jrpc--request): Protect against user-quits, cancelling timer --- lisp/progmodes/eglot.el | 51 ++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a2fcf5955c..5542902f743 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -624,13 +624,13 @@ objects, respectively. Wait TIMEOUT seconds for response or call nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the future, or to never at all, in case a new request with identical DEFERRED and for the same buffer overrides it (however, if that -happens, the original timeout keeps counting). Return the ID of -this request." +happens, the original timeout keeps counting). Return a list (ID +TIMER)." (let* ((id (eglot--next-request-id)) - (existing-timer nil) - (make-timeout + (timer nil) + (make-timer (lambda ( ) - (or existing-timer + (or timer (run-with-timer timeout nil (lambda () @@ -643,7 +643,7 @@ this request." (when deferred (let* ((buf (current-buffer)) (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) - (when existing (setq existing-timer (cadr existing))) + (when existing (setq existing (cadr existing))) (if (run-hook-with-args-until-failure 'eglot--ready-predicates deferred proc) (remhash (list deferred buf) (eglot--deferred-actions proc)) @@ -655,11 +655,15 @@ this request." (save-excursion (goto-char point) (apply #'eglot--async-request proc method params args))))))) - (puthash (list deferred buf) (list later (funcall make-timeout)) + (puthash (list deferred buf) (list later (setq timer (funcall make-timer))) (eglot--deferred-actions proc)) (cl-return-from eglot--async-request nil))))) ;; Really run it ;; + (eglot--process-send proc (eglot--obj :jsonrpc "2.0" + :id id + :method method + :params params)) (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) @@ -670,12 +674,9 @@ this request." (setf (eglot--status proc) `(,message t)) proc (eglot--obj :message "error ignored, status set" :id id :error code))) - (funcall make-timeout)) + (setq timer (funcall make-timer))) (eglot--pending-continuations proc)) - (eglot--process-send proc (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)))) + (list id timer))) (defun eglot--request (proc method params &optional deferred) "Like `eglot--async-request' for PROC, METHOD and PARAMS, but synchronous. @@ -685,18 +686,22 @@ DEFERRED is passed to `eglot--async-request', which see." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `eglot--ready-predicates'. (when deferred (eglot--signal-textDocument/didChange)) - (let* ((done (make-symbol "eglot--request-catch-tag")) + (let* ((done (make-symbol "eglot-catch")) id-and-timer (res - (catch done - (eglot--async-request - proc method params - :success-fn (lambda (result) (throw done `(done ,result))) - :timeout-fn (lambda () (throw done '(error "Timed out"))) - :error-fn (eglot--lambda (&key code message _data) - (throw done `(error - ,(format "Ooops: %s: %s" code message)))) - :deferred deferred) - (while t (accept-process-output nil 30))))) + (unwind-protect + (catch done + (setq + id-and-timer + (eglot--async-request + proc method params + :success-fn (lambda (result) (throw done `(done ,result))) + :timeout-fn (lambda () (throw done '(error "Timed out"))) + :error-fn (eglot--lambda (&key code message _data) + (throw done `(error + ,(format "Ooops: %s: %s" code message)))) + :deferred deferred)) + (while t (accept-process-output nil 30))) + (when (cadr id-and-timer) (cancel-timer (cadr id-and-timer)))))) (when (eq 'error (car res)) (eglot--error (cadr res))) (cadr res))) From 937f999a13a5767d49eabc54d8baa3a355bf4ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 10:24:18 +0100 Subject: [PATCH 158/771] If we're going to send rootpath, better send an absolute one javascript-typescript-langserver complained. * eglot.el (eglot--connect): Use expand-file-name. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5542902f743..981029071a8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -273,7 +273,8 @@ INTERACTIVE is t if inside interactive call." 'network) (emacs-pid)) :capabilities(eglot--client-capabilities) - :rootPath (car (project-roots project)) + :rootPath (expand-file-name + (car (project-roots project))) :rootUri (eglot--path-to-uri (car (project-roots project))) :initializationOptions [])) From a570c09fade804c55296c30deb2df3bb7842a97e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 11:12:41 +0100 Subject: [PATCH 159/771] Collect regions to change as markers, then edit * eglot.el (eglot--lsp-position-to-point): Accept MARKER optional arg. (eglot--range-region): Accept MARKERS optional arg. Return a list. (eglot--server-textDocument/publishDiagnostics) (eglot--hover-info, eglot-eldoc-function): eglot--range-region returns a list, not a cons. (eglot--apply-text-edits): First collect regions as markers, then edit. GitHub-reference: close https://github.com/joaotavora/eglot/issues/4 --- lisp/progmodes/eglot.el | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 981029071a8..3635e47b625 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -748,15 +748,16 @@ DEFERRED is passed to `eglot--async-request', which see." (- (goto-char (or pos (point))) (line-beginning-position))))) -(defun eglot--lsp-position-to-point (pos-plist) - "Convert LSP position POS-PLIST to Emacs point." +(defun eglot--lsp-position-to-point (pos-plist &optional marker) + "Convert LSP position POS-PLIST to Emacs point. +If optional MARKER, return a marker instead" (save-excursion (goto-char (point-min)) (forward-line (plist-get pos-plist :line)) (forward-char (min (plist-get pos-plist :character) (- (line-end-position) (line-beginning-position)))) - (point))) + (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) "URIfy PATH." @@ -798,10 +799,11 @@ DEFERRED is passed to `eglot--async-request', which see." "Determine if current server is capable of FEAT." (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) -(defun eglot--range-region (range) - "Return region (BEG . END) that represents LSP RANGE." - (cons (eglot--lsp-position-to-point (plist-get range :start)) - (eglot--lsp-position-to-point (plist-get range :end)))) +(defun eglot--range-region (range &optional markers) + "Return region (BEG END) that represents LSP RANGE. +If optional MARKERS, make markers." + (list (eglot--lsp-position-to-point (plist-get range :start) markers) + (eglot--lsp-position-to-point (plist-get range :end) markers))) ;;; Minor modes @@ -998,7 +1000,7 @@ function with the server still running." collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (pcase-let ((`(,beg ,end) (eglot--range-region range))) (flymake-make-diagnostic (current-buffer) beg end (cond ((<= severity 1) :error) @@ -1333,7 +1335,7 @@ DUMMY is ignored" (defun eglot--hover-info (contents &optional range) (concat (and range - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (pcase-let ((`(,beg ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append @@ -1413,7 +1415,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (setq eglot--highlights (when-buffer-window (mapcar (eglot--lambda (&key range _kind) - (pcase-let ((`(,beg . ,end) + (pcase-let ((`(,beg ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) @@ -1447,13 +1449,14 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (mapc (eglot--lambda (&key range newText) - (save-restriction - (widen) - (save-excursion - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (goto-char beg) (delete-region beg end) (insert newText))))) - edits) + (save-restriction + (widen) + (save-excursion + (mapc (eglot--lambda (newText beg end) + (goto-char beg) (delete-region beg end) (insert newText)) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits)))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) From 9606e5950a9d6b466bfb5ce78875c247d6560b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 13:12:08 +0100 Subject: [PATCH 160/771] * eglot.el (eglot-clear-status): remember to update modeline --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3635e47b625..9d5ef805350 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -589,7 +589,8 @@ is a symbol saying if this is a client or server originated." (defun eglot-clear-status (process) "Clear most recent error message from PROCESS." (interactive (list (eglot--current-process-or-lose))) - (setf (eglot--status process) nil)) + (setf (eglot--status process) nil) + (force-mode-line-update t)) (defun eglot--call-deferred (proc) "Call PROC's deferred actions, who may again defer themselves." From 75ca40724b869991b191e02e5930db21c1527dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 13:16:36 +0100 Subject: [PATCH 161/771] Better decide what text exactly to present as completions For inserting, :insertText takes precedence over :label. For annotating, first sentence of :documentation, then :detail, then :kind name. Also remember to send didChange in the :exit-function * eglot.el (eglot-completion-function): Rework main function and :annotation-function, and :exit-function --- lisp/progmodes/eglot.el | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9d5ef805350..e8af7ed80e6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -702,7 +702,7 @@ DEFERRED is passed to `eglot--async-request', which see." (throw done `(error ,(format "Ooops: %s: %s" code message)))) :deferred deferred)) - (while t (accept-process-output nil 30))) + (while t (accept-process-output nil 30))) (when (cadr id-and-timer) (cancel-timer (cadr id-and-timer)))))) (when (eq 'error (car res)) (eglot--error (cadr res))) (cadr res))) @@ -1300,15 +1300,19 @@ DUMMY is ignored" :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar - (eglot--lambda (&rest all &key label &allow-other-keys) - (add-text-properties 0 1 all label) label) + (eglot--lambda (&rest all &key label insertText &allow-other-keys) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) insert)) items)))) :annotation-function (lambda (obj) - (propertize (concat " " (or (get-text-property 0 :detail obj) - (cdr (assoc (get-text-property 0 :kind obj) - eglot--kind-names)))) - 'face 'font-lock-function-name-face)) + (cl-destructuring-bind (&key detail documentation kind &allow-other-keys) + (text-properties-at 0 obj) + (concat " " (propertize + (or (and documentation + (replace-regexp-in-string "\n.*" "" documentation)) + detail (cdr (assoc kind eglot--kind-names))) + 'face 'font-lock-function-name-face)))) :display-sort-function (lambda (items) (sort items (lambda (a b) @@ -1329,8 +1333,9 @@ DUMMY is ignored" (font-lock-ensure) (insert documentation) (current-buffer))))) - :exit-function - (lambda (_string _status) (eglot-eldoc-function)))))) + :exit-function (lambda (_string _status) + (eglot--signal-textDocument/didChange) + (eglot-eldoc-function)))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") From 212db69280a825473b8539b1fcafc67587d05d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 13:45:10 +0100 Subject: [PATCH 162/771] Check capabilities before sending :completionitem/resolve * eglot.el (eglot--server-capable): Rewrite. (eglot-completion-at-point): Check caps before sending :completionItem/resolve --- lisp/progmodes/eglot.el | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e8af7ed80e6..a5c46961317 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -796,9 +796,16 @@ If optional MARKER, return a marker instead" (font-lock-ensure) (buffer-string))))) -(defun eglot--server-capable (feat) - "Determine if current server is capable of FEAT." - (plist-get (eglot--capabilities (eglot--current-process-or-lose)) feat)) +(defun eglot--server-capable (&rest feats) + "Determine if current server is capable of FEATS." + (cl-loop for caps = (eglot--capabilities (eglot--current-process-or-lose)) + then (cadr probe) + for feat in feats + for probe = (plist-member caps feat) + if (not probe) do (cl-return nil) + if (eq (cadr probe) t) do (cl-return t) + if (eq (cadr probe) :json-false) do (cl-return nil) + finally (cl-return (or probe t)))) (defun eglot--range-region (range &optional markers) "Return region (BEG END) that represents LSP RANGE. @@ -1323,9 +1330,11 @@ DUMMY is ignored" (lambda (obj) (let ((documentation (or (get-text-property 0 :documentation obj) - (plist-get (eglot--request proc :completionItem/resolve - (text-properties-at 0 obj)) - :documentation)))) + (and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get (eglot--request proc :completionItem/resolve + (text-properties-at 0 obj)) + :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") (erase-buffer) From 5b8aa5c90851528bc8a04aaefc891fff6be6846b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 14:26:46 +0100 Subject: [PATCH 163/771] Robustness fixes for the request mechanism * eglot.el (eglot--async-request): Pass actual id to eglot--log-event (eglot--request): Also cancel any continuations. --- lisp/progmodes/eglot.el | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a5c46961317..13d008689ca 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -640,7 +640,7 @@ TIMER)." (funcall (or timeout-fn (lambda () (eglot--log-event - proc `(:timed-out ,method :id id + proc `(:timed-out ,method :id ,id :params ,params))))))))))) (when deferred (let* ((buf (current-buffer)) @@ -703,7 +703,9 @@ DEFERRED is passed to `eglot--async-request', which see." ,(format "Ooops: %s: %s" code message)))) :deferred deferred)) (while t (accept-process-output nil 30))) - (when (cadr id-and-timer) (cancel-timer (cadr id-and-timer)))))) + (pcase-let ((`(,id ,timer) id-and-timer)) + (when id (remhash id (eglot--pending-continuations proc))) + (when timer (cancel-timer timer)))))) (when (eq 'error (car res)) (eglot--error (cadr res))) (cadr res))) From 490ad2ce0b25cacf9e534af8d9c1ceadba6ddbe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 16:50:09 +0100 Subject: [PATCH 164/771] Handle managed buffers in own process var This should save some trouble when testing noninteractively. Because eglot--shutdown didn't turn off the minor mode, test code running immediately after it could still make didClose requests, for example. The sentinel was the previous responsible for turning off the minor mode and didn't get a chance to run in that case. Now eglot--shutdown is also responsible for turning off the minor mode. All this should be hidden behind eglot--managed-mode-onoff. * eglot.el (eglot--managed-buffers): New process-local variable. (eglot--process-sentinel): Turn off managed mode. (eglot--managed-mode-onoff): New function. (eglot--managed-mode): Don't offer to kill server here. (eglot--buffer-managed-p): Remove. (eglot--maybe-activate-editing-mode): Activate mode here. (eglot-shutdown): Turn off minor mode here. (eglot--server-window/progress): Simplify slightly. --- lisp/progmodes/eglot.el | 51 +++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13d008689ca..5829aff59b6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -172,6 +172,9 @@ A list (WHAT SERIOUS-P).") (eglot--define-process-var eglot--file-watches (make-hash-table :test #'equal) "File system watches for the didChangeWatchedfiles thingy.") +(eglot--define-process-var eglot--managed-buffers nil + "Buffers managed by the server.") + (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is used to name the the started process or connection. @@ -415,10 +418,8 @@ INTERACTIVE is t if called interactively." (funcall error `(:code -1 :message "Server died")))) (eglot--pending-continuations proc)) ;; Turn off `eglot--managed-mode' where appropriate. - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (when (eglot--buffer-managed-p proc) - (eglot--managed-mode -1)))) + (dolist (buffer (eglot--managed-buffers proc)) + (with-current-buffer buffer (eglot--managed-mode-onoff proc -1))) ;; Forget about the process-project relationship (setf (gethash (eglot--project proc) eglot--processes-by-project) (delq proc @@ -849,19 +850,20 @@ If optional MARKERS, make markers." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (remove-function (local imenu-create-index-function) #'eglot-imenu) - (let ((proc (eglot--current-process))) - (when (and (process-live-p proc) (y-or-n-p "[eglot] Kill server too? ")) - (eglot-shutdown proc t)))))) + (remove-function (local imenu-create-index-function) #'eglot-imenu)))) + +(defun eglot--managed-mode-onoff (proc arg) + "Proxy for function `eglot--managed-mode' with ARG and PROC." + (eglot--managed-mode arg) + (let ((buf (current-buffer))) + (if eglot--managed-mode + (cl-pushnew buf (eglot--managed-buffers proc)) + (setf (eglot--managed-buffers proc) + (delq buf (eglot--managed-buffers proc)))))) (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) -(defun eglot--buffer-managed-p (&optional proc) - "Tell if current buffer is managed by PROC." - (and buffer-file-name (let ((cur (eglot--current-process))) - (or (and (null proc) cur) - (and proc (eq proc cur)))))) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -871,11 +873,14 @@ If optional MARKERS, make markers." If PROC is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." ;; Called even when revert-buffer-in-progress-p - (when (eglot--buffer-managed-p proc) - (eglot--managed-mode 1) - (eglot--signal-textDocument/didOpen) - (flymake-start) - (funcall (or eglot--current-flymake-report-fn #'ignore) nil))) + (let* ((cur (and buffer-file-name (eglot--current-process))) + (proc (or (and (null proc) cur) + (and proc (eq proc cur) cur)))) + (when proc + (eglot--managed-mode-onoff proc 1) + (eglot--signal-textDocument/didOpen) + (flymake-start) + (funcall (or eglot--current-flymake-report-fn #'ignore) nil)))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -960,6 +965,9 @@ function with the server still running." (eglot--request proc :shutdown nil) ;; this one is supposed to always fail, hence ignore-errors (ignore-errors (eglot--request proc :exit nil))) + ;; Turn off `eglot--managed-mode' where appropriate. + (dolist (buffer (eglot--managed-buffers proc)) + (with-current-buffer buffer (eglot--managed-mode-onoff proc -1))) (when (process-live-p proc) (eglot--warn "Brutally deleting non-compliant existing process %s" proc) (delete-process proc)))) @@ -1589,11 +1597,10 @@ Proceed? " "Handle notification window/progress" (setf (eglot--spinner process) (list id title done message)) (when (and (equal "Indexing" title) done) - (dolist (buffer (buffer-list)) + (dolist (buffer (eglot--managed-buffers process)) (with-current-buffer buffer - (when (eglot--buffer-managed-p process) - (funcall (or eglot--current-flymake-report-fn #'ignore) - eglot--unreported-diagnostics)))))) + (funcall (or eglot--current-flymake-report-fn #'ignore) + eglot--unreported-diagnostics))))) (provide 'eglot) ;;; eglot.el ends here From 749e83c57c6e51a8111c0a960e6ac56a69238db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 17:04:46 +0100 Subject: [PATCH 165/771] Add some completion tests for pyls * eglot-tests.el (edebug): Require it. (eglot--call-with-dirs-and-files): Simplify. (eglot--call-with-test-timeout): Don't timeout if edebug. (auto-detect-running-server, auto-reconnect): Skip unless rls is found. (basic-completions): New test. (hover-after-completions): New failing test. * eglot.el (eglot-eldoc-function): Force write eldoc-last-message, for tests sake. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5829aff59b6..e9da5231696 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1429,8 +1429,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." proc :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) (unless sig-showing - (when-buffer-window - (eldoc-message (eglot--hover-info contents range))))) + (setq eldoc-last-message (eglot--hover-info contents range)) + (when-buffer-window (eldoc-message eldoc-last-message)))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request From 65ed542c78b3249b9548fa36d98debaa0ab9cfb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 May 2018 23:41:14 +0100 Subject: [PATCH 166/771] * eglot.el (version): bump to 0.4 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e9da5231696..7bfc8a91aae 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.3 +;; Version: 0.4 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From baf1b82eaa7aed01b47baee8a787852f642c2542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 20 May 2018 00:35:11 +0100 Subject: [PATCH 167/771] Rewrite a couple of defs and shave a yak * eglot.el (eglot--define-process-var): Simplify. (eglot--format-markup): Rewrite. (eglot--warn, eglot--pos-to-lsp-position) (eglot--lsp-position-to-point, eglot--server-capable) (eglot--maybe-activate-editing-mode) (eglot--server-textDocument/publishDiagnostics) (eglot--server-workspace/applyEdit, eglot--hover-info): Yak shaving. --- lisp/progmodes/eglot.el | 111 ++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 68 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7bfc8a91aae..72e5d31473a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -109,21 +109,16 @@ lasted more than that many seconds." "Return the current EGLOT process or error." (or (eglot--current-process) (eglot--error "No current EGLOT process"))) -(defmacro eglot--define-process-var - (var-sym initval &optional doc) +(defmacro eglot--define-process-var (var-sym initval &optional doc) "Define VAR-SYM as a generalized process-local variable. INITVAL is the default value. DOC is the documentation." - (declare (indent 2)) + (declare (indent 2) (doc-string 3)) `(progn - (put ',var-sym 'function-documentation ,doc) (defun ,var-sym (proc) - (let* ((plist (process-plist proc)) - (probe (plist-member plist ',var-sym))) - (if probe - (cadr probe) - (let ((def ,initval)) - (process-put proc ',var-sym def) - def)))) + ,doc (let* ((plist (process-plist proc)) + (probe (plist-member plist ',var-sym))) + (if probe (cadr probe) + (let ((def ,initval)) (process-put proc ',var-sym def) def)))) (gv-define-setter ,var-sym (to-store process) `(let ((once ,to-store)) (process-put ,process ',',var-sym once) once)))) @@ -738,29 +733,23 @@ DEFERRED is passed to `eglot--async-request', which see." "Warning message with FORMAT and ARGS." (apply #'eglot--message (concat "(warning) " format) args) (let ((warning-minimum-level :error)) - (display-warning 'eglot - (apply #'format format args) - :warning))) + (display-warning 'eglot (apply #'format format args) :warning))) (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion - (eglot--obj :line - ;; F!@(#*&#$)CKING OFF-BY-ONE - (1- (line-number-at-pos pos t)) - :character - (- (goto-char (or pos (point))) - (line-beginning-position))))) + (eglot--obj :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE + :character (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. If optional MARKER, return a marker instead" (save-excursion (goto-char (point-min)) (forward-line (plist-get pos-plist :line)) - (forward-char - (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) + (forward-char (min (plist-get pos-plist :character) + (- (line-end-position) + (line-beginning-position)))) (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) @@ -784,31 +773,24 @@ If optional MARKER, return a marker instead" (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." - (cond ((stringp markup) - (with-temp-buffer - (ignore-errors (funcall (intern "markdown-mode"))) ;escape bytecomp - (font-lock-ensure) - (insert markup) - (string-trim (buffer-string)))) - (t - (with-temp-buffer - (ignore-errors (funcall (intern (concat - (plist-get markup :language) - "-mode" )))) - (insert (plist-get markup :value)) - (font-lock-ensure) - (buffer-string))))) + (pcase-let ((`(,string ,mode) + (if (stringp markup) (list (string-trim markup) + (intern "markdown-mode")) + (list (plist-get markup :value) + (intern (concat (plist-get markup :language) "-mode" )))))) + (with-temp-buffer + (funcall mode) (insert string) (font-lock-ensure) (buffer-string)))) (defun eglot--server-capable (&rest feats) - "Determine if current server is capable of FEATS." - (cl-loop for caps = (eglot--capabilities (eglot--current-process-or-lose)) - then (cadr probe) - for feat in feats - for probe = (plist-member caps feat) - if (not probe) do (cl-return nil) - if (eq (cadr probe) t) do (cl-return t) - if (eq (cadr probe) :json-false) do (cl-return nil) - finally (cl-return (or probe t)))) +"Determine if current server is capable of FEATS." +(cl-loop for caps = (eglot--capabilities (eglot--current-process-or-lose)) + then (cadr probe) + for feat in feats + for probe = (plist-member caps feat) + if (not probe) do (cl-return nil) + if (eq (cadr probe) t) do (cl-return t) + if (eq (cadr probe) :json-false) do (cl-return nil) + finally (cl-return (or probe t)))) (defun eglot--range-region (range &optional markers) "Return region (BEG END) that represents LSP RANGE. @@ -864,7 +846,6 @@ If optional MARKERS, make markers." (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) - (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -874,8 +855,7 @@ If PROC is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." ;; Called even when revert-buffer-in-progress-p (let* ((cur (and buffer-file-name (eglot--current-process))) - (proc (or (and (null proc) cur) - (and proc (eq proc cur) cur)))) + (proc (or (and (null proc) cur) (and proc (eq proc cur) cur)))) (when proc (eglot--managed-mode-onoff proc 1) (eglot--signal-textDocument/didOpen) @@ -1009,7 +989,7 @@ function with the server still running." "Unreported diagnostics for this buffer.") (cl-defun eglot--server-textDocument/publishDiagnostics - (_process &key uri diagnostics) + (_proc &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1063,15 +1043,12 @@ THINGS are either registrations or unregisterations." (proc &key id _label edit) "Handle server request workspace/applyEdit" (condition-case err - (progn - (eglot--apply-workspace-edit edit 'confirm) - (eglot--reply proc id :result `(:applied ))) - (error - (eglot--reply proc id - :result `(:applied :json-false) - :error - (eglot--obj :code -32001 - :message (format "%s" err)))))) + (progn (eglot--apply-workspace-edit edit 'confirm) + (eglot--reply proc id :result `(:applied ))) + (error (eglot--reply proc id + :result `(:applied :json-false) + :error (eglot--obj :code -32001 + :message (format "%s" err)))))) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." @@ -1359,15 +1336,13 @@ DUMMY is ignored" (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") (defun eglot--hover-info (contents &optional range) - (concat (and range - (pcase-let ((`(,beg ,end) (eglot--range-region range))) - (concat (buffer-substring beg end) ": "))) + (concat (and range (pcase-let ((`(,beg ,end) (eglot--range-region range))) + (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup - (append - (cond ((vectorp contents) - contents) - (contents - (list contents)))) "\n"))) + (append (cond ((vectorp contents) + contents) + (contents + (list contents)))) "\n"))) (defun eglot--sig-info (sigs active-sig active-param) (cl-loop From 28b199c3448104161aa93ea0ec0732c367421c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 20 May 2018 00:44:21 +0100 Subject: [PATCH 168/771] Fix a bug introduced in the previous commit * eglot.el (eglot--format-markup): Ignore errors when calling possibly unknown functions. (eglot-completion-at-point): Use eglot--format-markup (eglot--hover-info): Yak shaving --- lisp/progmodes/eglot.el | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 72e5d31473a..9d7253af26d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -779,7 +779,8 @@ If optional MARKER, return a marker instead" (list (plist-get markup :value) (intern (concat (plist-get markup :language) "-mode" )))))) (with-temp-buffer - (funcall mode) (insert string) (font-lock-ensure) (buffer-string)))) + (ignore-errors (funcall mode)) + (insert string) (font-lock-ensure) (buffer-string)))) (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." @@ -1324,10 +1325,7 @@ DUMMY is ignored" :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") - (erase-buffer) - (ignore-errors (funcall (intern "markdown-mode"))) - (font-lock-ensure) - (insert documentation) + (insert (eglot--format-markup documentation)) (current-buffer))))) :exit-function (lambda (_string _status) (eglot--signal-textDocument/didChange) @@ -1339,10 +1337,8 @@ DUMMY is ignored" (concat (and range (pcase-let ((`(,beg ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup - (append (cond ((vectorp contents) - contents) - (contents - (list contents)))) "\n"))) + (append (cond ((vectorp contents) contents) + (contents (list contents)))) "\n"))) (defun eglot--sig-info (sigs active-sig active-param) (cl-loop From 1b62dfd97b0d7fbbf18c7c39ede9639daab66024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 20 May 2018 13:21:12 +0100 Subject: [PATCH 169/771] Rename jrpc.el to jsonrpc.el * eglot.el [everywhere]: jrpc -> jsonrpc everywhere. Reindent. * eglot-tests [everywhere]: jrpc -> jsonrpc everywhere. * jsonrpc.el: New file. * Makefile: jrpc.el -> jsonrpc.el --- lisp/progmodes/eglot.el | 359 ++++++++++++++++++++-------------------- 1 file changed, 183 insertions(+), 176 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 81229a58177..907c98b2c08 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -59,7 +59,7 @@ (require 'flymake) (require 'xref) (require 'subr-x) -(require 'jrpc) +(require 'jsonrpc) (require 'filenotify) @@ -98,26 +98,26 @@ lasted more than that many seconds." (defvar eglot--processes-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(jrpc-define-process-var eglot--major-mode nil +(jsonrpc-define-process-var eglot--major-mode nil "The major-mode this server is managing.") -(jrpc-define-process-var eglot--capabilities :unreported +(jsonrpc-define-process-var eglot--capabilities :unreported "Holds list of capabilities that server reported") -(jrpc-define-process-var eglot--project nil +(jsonrpc-define-process-var eglot--project nil "The project the server belongs to.") -(jrpc-define-process-var eglot--spinner `(nil nil t) +(jsonrpc-define-process-var eglot--spinner `(nil nil t) "\"Spinner\" used by some servers. A list (ID WHAT DONE-P).") -(jrpc-define-process-var eglot--moribund nil +(jsonrpc-define-process-var eglot--moribund nil "Non-nil if server is about to exit") -(jrpc-define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect +(jsonrpc-define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect "If non-nil, don't autoreconnect on unexpected quit.") -(jrpc-define-process-var eglot--file-watches (make-hash-table :test #'equal) +(jsonrpc-define-process-var eglot--file-watches (make-hash-table :test #'equal) "File system watches for the didChangeWatchedfiles thingy.") (defun eglot--on-shutdown (proc) @@ -146,14 +146,14 @@ A list (ID WHAT DONE-P).") Forcefully quit it if it doesn't respond. Don't leave this function with the server still running. INTERACTIVE is t if called interactively." - (interactive (list (jrpc-current-process-or-lose) t)) + (interactive (list (jsonrpc-current-process-or-lose) t)) (when interactive (eglot--message "Asking %s politely to terminate" proc)) (unwind-protect - (let ((jrpc-request-timeout 3)) + (let ((jsonrpc-request-timeout 3)) (setf (eglot--moribund proc) t) - (jrpc-request proc :shutdown nil) + (jsonrpc-request proc :shutdown nil) ;; this one should always fail under normal conditions - (ignore-errors (jrpc-request proc :exit nil))) + (ignore-errors (jsonrpc-request proc :exit nil))) (when (process-live-p proc) (eglot--warn "Brutally deleting existing process %s" proc) (delete-process proc)))) @@ -178,14 +178,14 @@ called interactively." (defun eglot--client-capabilities () "What the EGLOT LSP client supports." - (jrpc-obj - :workspace (jrpc-obj + (jsonrpc-obj + :workspace (jsonrpc-obj :applyEdit t :workspaceEdit `(:documentChanges :json-false) :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) - :textDocument (jrpc-obj - :synchronization (jrpc-obj + :textDocument (jsonrpc-obj + :synchronization (jsonrpc-obj :dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t) :completion `(:dynamicRegistration :json-false) @@ -197,7 +197,7 @@ called interactively." :documentHighlight `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (jrpc-obj))) + :experimental (jsonrpc-obj))) (defvar eglot--command-history nil "History of CONTACT arguments to `eglot'.") @@ -282,7 +282,7 @@ MANAGED-MAJOR-MODE is an Emacs major mode. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) (let* ((short-name (eglot--project-short-name project))) - (let ((current-process (jrpc-current-process))) + (let ((current-process (jsonrpc-current-process))) (if (and (process-live-p current-process) interactive (y-or-n-p "[eglot] Live process found, reconnect instead? ")) @@ -301,32 +301,33 @@ managing `%s' buffers in project `%s'." (defun eglot-reconnect (process &optional interactive) "Reconnect to PROCESS. INTERACTIVE is t if called interactively." - (interactive (list (jrpc-current-process-or-lose) t)) + (interactive (list (jsonrpc-current-process-or-lose) t)) (when (process-live-p process) (eglot-shutdown process interactive)) (eglot--connect (eglot--project process) (eglot--major-mode process) - (jrpc-name process) - (jrpc-contact process)) + (jsonrpc-name process) + (jsonrpc-contact process)) (eglot--message "Reconnected!")) -(defalias 'eglot-events-buffer 'jrpc-events-buffer) +(defalias 'eglot-events-buffer 'jsonrpc-events-buffer) (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") (defun eglot--dispatch (proc method id params) - "Dispatcher passed to `jrpc-connect'. + "Dispatcher passed to `jsonrpc-connect'. Builds a function from METHOD, passes it PROC, ID and PARAMS." (let* ((handler-sym (intern (format "eglot--server-%s" method)))) (if (functionp handler-sym) ;; FIXME: fails if params is array, not object (apply handler-sym proc (append params (if id `(:id ,id)))) - (jrpc-reply proc id - :error (jrpc-obj :code -32601 :message "Unimplemented"))) + (jsonrpc-reply proc id + :error (jsonrpc-obj :code -32601 :message "Unimplemented"))) (force-mode-line-update t))) (defun eglot--connect (project managed-major-mode name contact) (let* ((contact (if (functionp contact) (funcall contact) contact)) - (proc (jrpc-connect name contact #'eglot--dispatch #'eglot--on-shutdown)) + (proc + (jsonrpc-connect name contact #'eglot--dispatch #'eglot--on-shutdown)) success) (setf (eglot--project proc) project) (setf (eglot--major-mode proc)managed-major-mode) @@ -334,23 +335,23 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (run-hook-with-args 'eglot-connect-hook proc) (unwind-protect (cl-destructuring-bind (&key capabilities) - (jrpc-request + (jsonrpc-request proc :initialize - (jrpc-obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :rootPath (car (project-roots project)) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [] - :capabilities (eglot--client-capabilities))) + (jsonrpc-obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :rootPath (car (project-roots project)) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [] + :capabilities (eglot--client-capabilities))) (setf (eglot--capabilities proc) capabilities) - (setf (jrpc-status proc) nil) + (setf (jsonrpc-status proc) nil) (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode proc))) - (jrpc-notify proc :initialized (jrpc-obj :__dummy__ t)) + (jsonrpc-notify proc :initialized (jsonrpc-obj :__dummy__ t)) (setf (eglot--inhibit-autoreconnect proc) (cond ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) @@ -389,12 +390,12 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion - (jrpc-obj :line - ;; F!@(#*&#$)CKING OFF-BY-ONE - (1- (line-number-at-pos pos t)) - :character - (- (goto-char (or pos (point))) - (line-beginning-position))))) + (jsonrpc-obj :line + ;; F!@(#*&#$)CKING OFF-BY-ONE + (1- (line-number-at-pos pos t)) + :character + (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist) "Convert LSP position POS-PLIST to Emacs point." @@ -444,7 +445,7 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." (defun eglot--server-capable (feat) "Determine if current server is capable of FEAT." - (plist-get (eglot--capabilities (jrpc-current-process-or-lose)) feat)) + (plist-get (eglot--capabilities (jsonrpc-current-process-or-lose)) feat)) (defun eglot--range-region (range) "Return region (BEG . END) that represents LSP RANGE." @@ -461,8 +462,8 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." nil nil eglot-mode-map (cond (eglot--managed-mode - (add-hook 'jrpc-find-process-functions 'eglot--find-current-process nil t) - (add-hook 'jrpc-ready-predicates 'eglot--server-ready-p nil t) + (add-hook 'jsonrpc-find-process-functions 'eglot--find-current-process nil t) + (add-hook 'jsonrpc-ready-predicates 'eglot--server-ready-p nil t) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -476,8 +477,8 @@ Builds a function from METHOD, passes it PROC, ID and PARAMS." #'eglot-eldoc-function) (add-function :around (local imenu-create-index-function) #'eglot-imenu)) (t - (remove-hook 'jrpc-find-process-functions 'eglot--find-current-process t) - (remove-hook 'jrpc-ready-predicates 'eglot--server-ready-p t) + (remove-hook 'jsonrpc-find-process-functions 'eglot--find-current-process t) + (remove-hook 'jsonrpc-ready-predicates 'eglot--server-ready-p t) (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -549,11 +550,11 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((proc (jrpc-current-process)) - (name (and (process-live-p proc) (jrpc-name proc))) - (pending (and proc (length (jrpc-outstanding-request-ids proc)))) + (pcase-let* ((proc (jsonrpc-current-process)) + (name (and (process-live-p proc) (jsonrpc-name proc))) + (pending (and proc (length (jsonrpc-outstanding-request-ids proc)))) (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) - (`(,status ,serious-p) (and proc (jrpc-status proc)))) + (`(,status ,serious-p) (and proc (jsonrpc-status proc)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) (when name @@ -609,10 +610,10 @@ Uses THING, FACE, DEFS and PREPEND." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (jrpc-reply process id :result (jrpc-obj :title reply)) - (jrpc-reply process id - :error (jrpc-obj :code -32800 - :message "User cancelled")))))) + (jsonrpc-reply process id :result (jsonrpc-obj :title reply)) + (jsonrpc-reply process id + :error (jsonrpc-obj :code -32800 + :message "User cancelled")))))) (cl-defun eglot--server-window/logMessage (_proc &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer @@ -659,10 +660,10 @@ THINGS are either registrations or unregisterations." proc :id id registerOptions)) (unless (eq t (car retval)) (cl-return-from eglot--register-unregister - (jrpc-reply + (jsonrpc-reply proc jsonrpc-id :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (jrpc-reply proc jsonrpc-id :result (jrpc-obj :message "OK"))) + (jsonrpc-reply proc jsonrpc-id :result (jsonrpc-obj :message "OK"))) (cl-defun eglot--server-client/registerCapability (proc &key id registrations) @@ -680,42 +681,42 @@ THINGS are either registrations or unregisterations." (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) - (jrpc-reply proc id :result `(:applied ))) + (jsonrpc-reply proc id :result `(:applied ))) (error - (jrpc-reply proc id - :result `(:applied :json-false) - :error - (jrpc-obj :code -32001 - :message (format "%s" err)))))) + (jsonrpc-reply proc id + :result `(:applied :json-false) + :error + (jsonrpc-obj :code -32001 + :message (format "%s" err)))))) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." - (jrpc-obj :uri (eglot--path-to-uri buffer-file-name))) + (jsonrpc-obj :uri (eglot--path-to-uri buffer-file-name))) (defvar-local eglot--versioned-identifier 0) (defun eglot--VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (append (eglot--TextDocumentIdentifier) - (jrpc-obj :version eglot--versioned-identifier))) + (jsonrpc-obj :version eglot--versioned-identifier))) (defun eglot--TextDocumentItem () "Compute TextDocumentItem object for current buffer." (append (eglot--VersionedTextDocumentIdentifier) - (jrpc-obj :languageId - (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode)) - "unknown") - :text - (save-restriction - (widen) - (buffer-substring-no-properties (point-min) (point-max)))))) + (jsonrpc-obj :languageId + (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode)) + "unknown") + :text + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." - (jrpc-obj :textDocument (eglot--TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position))) + (jsonrpc-obj :textDocument (eglot--TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") @@ -747,16 +748,16 @@ Records START, END and PRE-CHANGE-LENGTH locally." ;; HACK! Launching a deferred sync request with outstanding changes is a ;; bad idea, since that might lead to the request never having a -;; chance to run, because `jrpc-ready-predicates'. -(advice-add #'jrpc-request :before +;; chance to run, because `jsonrpc-ready-predicates'. +(advice-add #'jsonrpc-request :before (cl-function (lambda (_proc _method _params &key deferred) - (when (and eglot--managed-mode deferred) - (eglot--signal-textDocument/didChange))))) + (when (and eglot--managed-mode deferred) + (eglot--signal-textDocument/didChange))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when (eglot--outstanding-edits-p) - (let* ((proc (jrpc-current-process-or-lose)) + (let* ((proc (jsonrpc-current-process-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) (emacs-messup (/= (length (car eglot--recent-changes)) (length (cdr eglot--recent-changes)))) @@ -765,58 +766,58 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--warn "`eglot--recent-changes' messup: %s" eglot--recent-changes)) (save-restriction (widen) - (jrpc-notify + (jsonrpc-notify proc :textDocument/didChange - (jrpc-obj + (jsonrpc-obj :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector - (jrpc-obj + (jsonrpc-obj :text (buffer-substring-no-properties (point-min) (point-max)))) (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) for (len after-text) across (cdr eglot--recent-changes) - vconcat `[,(jrpc-obj :range (jrpc-obj :start start-pos - :end end-pos) - :rangeLength len - :text after-text)]))))) + vconcat `[,(jsonrpc-obj :range (jsonrpc-obj :start start-pos + :end end-pos) + :rangeLength len + :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) (setf (eglot--spinner proc) (list nil :textDocument/didChange t)) ;; HACK! - (jrpc--call-deferred proc)))) + (jsonrpc--call-deferred proc)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (setq eglot--recent-changes (cons [] [])) - (jrpc-notify - (jrpc-current-process-or-lose) + (jsonrpc-notify + (jsonrpc-current-process-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (jrpc-notify - (jrpc-current-process-or-lose) + (jsonrpc-notify + (jsonrpc-current-process-or-lose) :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (let ((proc (jrpc-current-process-or-lose)) + (let ((proc (jsonrpc-current-process-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) - (jrpc-notify proc :textDocument/willSave params) + (jsonrpc-notify proc :textDocument/willSave params) (ignore-errors - (let ((jrpc-request-timeout 0.5)) + (let ((jsonrpc-request-timeout 0.5)) (when (plist-get :willSaveWaitUntil (eglot--server-capable :textDocumentSync)) (eglot--apply-text-edits - (jrpc-request proc :textDocument/willSaveWaituntil params))))))) + (jsonrpc-request proc :textDocument/willSaveWaituntil params))))))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." - (jrpc-notify - (jrpc-current-process-or-lose) + (jsonrpc-notify + (jsonrpc-current-process-or-lose) :textDocument/didSave - (jrpc-obj + (jsonrpc-obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) :textDocument (eglot--TextDocumentIdentifier)))) @@ -856,26 +857,27 @@ DUMMY is ignored" (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) - (let ((proc (jrpc-current-process-or-lose)) + (let ((proc (jsonrpc-current-process-or-lose)) (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) (setq eglot--xref-known-symbols (mapcar - (jrpc-lambda (&key name kind location containerName) + (jsonrpc-lambda + (&key name kind location containerName) (propertize name :textDocumentPositionParams - (jrpc-obj :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) + (jsonrpc-obj :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) :locations (list location) :kind kind :containerName containerName)) - (jrpc-request proc - :textDocument/documentSymbol - (jrpc-obj - :textDocument text-id)))) + (jsonrpc-request proc + :textDocument/documentSymbol + (jsonrpc-obj + :textDocument text-id)))) (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) @@ -890,13 +892,13 @@ DUMMY is ignored" (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (jrpc-request (jrpc-current-process-or-lose) - :textDocument/definition - (get-text-property - 0 :textDocumentPositionParams identifier))))) - (mapcar (jrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - location-or-locations))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :textDocument/definition + (get-text-property + 0 :textDocumentPositionParams identifier))))) + (mapcar (jsonrpc-lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) @@ -908,42 +910,42 @@ DUMMY is ignored" (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) (mapcar - (jrpc-lambda (&key uri range) + (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (jrpc-request (jrpc-current-process-or-lose) - :textDocument/references - (append - params - (jrpc-obj :context - (jrpc-obj :includeDeclaration t))))))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :textDocument/references + (append + params + (jsonrpc-obj :context + (jsonrpc-obj :includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) (mapcar - (jrpc-lambda (&key name location &allow-other-keys) + (jsonrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) - (jrpc-request (jrpc-current-process-or-lose) - :workspace/symbol - (jrpc-obj :query pattern))))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :workspace/symbol + (jsonrpc-obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (proc (jrpc-current-process-or-lose))) + (proc (jsonrpc-current-process-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) (completion-table-with-cache (lambda (_ignored) - (let* ((resp (jrpc-request proc - :textDocument/completion - (eglot--TextDocumentPositionParams) - :deferred :textDocument/completion)) + (let* ((resp (jsonrpc-request proc + :textDocument/completion + (eglot--TextDocumentPositionParams) + :deferred :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar - (jrpc-lambda (&rest all &key label &allow-other-keys) + (jsonrpc-lambda (&rest all &key label &allow-other-keys) (add-text-properties 0 1 all label) label) items)))) :annotation-function @@ -962,8 +964,8 @@ DUMMY is ignored" (lambda (obj) (let ((documentation (or (get-text-property 0 :documentation obj) - (plist-get (jrpc-request proc :completionItem/resolve - (text-properties-at 0 obj)) + (plist-get (jsonrpc-request proc :completionItem/resolve + (text-properties-at 0 obj)) :documentation)))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") @@ -1014,8 +1016,8 @@ DUMMY is ignored" "Request \"hover\" information for the thing at point." (interactive) (cl-destructuring-bind (&key contents range) - (jrpc-request (jrpc-current-process-or-lose) :textDocument/hover - (eglot--TextDocumentPositionParams)) + (jsonrpc-request (jsonrpc-current-process-or-lose) :textDocument/hover + (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) (with-help-window "*eglot help*" (with-current-buffer standard-output @@ -1025,48 +1027,51 @@ DUMMY is ignored" "EGLOT's `eldoc-documentation-function' function. If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((buffer (current-buffer)) - (proc (jrpc-current-process-or-lose)) + (proc (jsonrpc-current-process-or-lose)) (position-params (eglot--TextDocumentPositionParams)) sig-showing) (cl-macrolet ((when-buffer-window (&body body) `(when (get-buffer-window buffer) (with-current-buffer buffer ,@body)))) (when (eglot--server-capable :signatureHelpProvider) - (jrpc-async-request + (jsonrpc-async-request proc :textDocument/signatureHelp position-params - :success-fn (jrpc-lambda (&key signatures activeSignature - activeParameter) - (when-buffer-window - (when (cl-plusp (length signatures)) - (setq sig-showing t) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter))))) + :success-fn + (jsonrpc-lambda (&key signatures activeSignature + activeParameter) + (when-buffer-window + (when (cl-plusp (length signatures)) + (setq sig-showing t) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter))))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) - (jrpc-async-request + (jsonrpc-async-request proc :textDocument/hover position-params - :success-fn (jrpc-lambda (&key contents range) + :success-fn (jsonrpc-lambda (&key contents range) (unless sig-showing (when-buffer-window - (eldoc-message (eglot--hover-info contents range))))) + (eldoc-message + (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) - (jrpc-async-request + (jsonrpc-async-request proc :textDocument/documentHighlight position-params - :success-fn (lambda (highlights) - (mapc #'delete-overlay eglot--highlights) - (setq eglot--highlights - (when-buffer-window - (mapcar - (jrpc-lambda (&key range _kind) - (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)))) + :success-fn + (lambda (highlights) + (mapc #'delete-overlay eglot--highlights) + (setq eglot--highlights + (when-buffer-window + (mapcar + (jsonrpc-lambda (&key range _kind) + (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) @@ -1075,14 +1080,15 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eglot--server-capable :documentSymbolProvider) (let ((entries (mapcar - (jrpc-lambda (&key name kind location _containerName) + (jsonrpc-lambda + (&key name kind location _containerName) (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (jrpc-request (jrpc-current-process-or-lose) - :textDocument/documentSymbol - (jrpc-obj - :textDocument (eglot--TextDocumentIdentifier)))))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :textDocument/documentSymbol + (jsonrpc-obj + :textDocument (eglot--TextDocumentIdentifier)))))) (append (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries) @@ -1094,7 +1100,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (mapc (jrpc-lambda (&key range newText) + (mapc (jsonrpc-lambda + (&key range newText) (save-restriction (widen) (save-excursion @@ -1146,9 +1153,9 @@ Proceed? " (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (jrpc-request (jrpc-current-process-or-lose) - :textDocument/rename `(,@(eglot--TextDocumentPositionParams) - ,@(jrpc-obj :newName newname))) + (jsonrpc-request (jsonrpc-current-process-or-lose) + :textDocument/rename `(,@(eglot--TextDocumentPositionParams) + ,@(jsonrpc-obj :newName newname))) current-prefix-arg)) @@ -1170,7 +1177,7 @@ Proceed? " (string-match (wildcard-to-regexp (expand-file-name glob)) f)))) - (jrpc-notify + (jsonrpc-notify proc :workspace/didChangeWatchedFiles `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) :type ,(cl-case action @@ -1209,7 +1216,7 @@ Proceed? " (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) (defun eglot--setup-rls-idiosyncrasies () "Prepare `eglot' to deal with RLS's special treatment." - (add-hook 'jrpc-ready-predicates 'eglot--rls-probably-ready-for-p t t))) + (add-hook 'jsonrpc-ready-predicates 'eglot--rls-probably-ready-for-p t t))) (cl-defun eglot--server-window/progress (process &key id done title message &allow-other-keys) From 3a127eed7b76ec02f4f408cbb03ab46ed2ac48b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 20 May 2018 15:24:55 +0100 Subject: [PATCH 170/771] Get rid of jsonrpc.el customization group and timeout * eglot.el (eglot-shutdown, eglot--signal-textDocument/willSave): Pass :timeout to jsonrpc-request. (defadvice jsonrpc-request): Add :timeout kwarg * jsonrpc.el (defgroup jsonrpc, jsonrpc-request-timeout): Remove. (jrpc-default-request-timeout): New constant. (jsonrpc-async-request): Use it. (jsonrpc-request): Accept timeout kwarg and pass it on. --- lisp/progmodes/eglot.el | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 41e57b4bb53..020e352c440 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -148,9 +148,9 @@ called interactively." (interactive (list (jsonrpc-current-process-or-lose) t)) (eglot--message "Asking %s politely to terminate" proc) (unwind-protect - (let ((jsonrpc-request-timeout 3)) + (progn (setf (eglot--moribund proc) t) - (jsonrpc-request proc :shutdown nil) + (jsonrpc-request proc :shutdown nil :timeout 3) ;; this one should always fail, hence ignore-errors (ignore-errors (jsonrpc-request proc :exit nil))) ;; Turn off `eglot--managed-mode' where appropriate. @@ -749,7 +749,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `jsonrpc-ready-predicates'. (advice-add #'jsonrpc-request :before - (cl-function (lambda (_proc _method _params &key deferred) + (cl-function (lambda (_proc _method _params &key deferred _timeout) (when (and eglot--managed-mode deferred) (eglot--signal-textDocument/didChange)))) '((name . eglot--signal-textDocument/didChange))) @@ -805,12 +805,11 @@ Records START, END and PRE-CHANGE-LENGTH locally." (let ((proc (jsonrpc-current-process-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) (jsonrpc-notify proc :textDocument/willSave params) - (ignore-errors - (let ((jsonrpc-request-timeout 0.5)) - (when (plist-get :willSaveWaitUntil - (eglot--server-capable :textDocumentSync)) - (eglot--apply-text-edits - (jsonrpc-request proc :textDocument/willSaveWaituntil params))))))) + (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) + (ignore-errors + (eglot--apply-text-edits + (jsonrpc-request proc :textDocument/willSaveWaituntil params + :timeout 0.5)))))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." From 7938af0c4a4b084b9eb29505ddeea78ae39d208d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 21 May 2018 09:00:49 +0100 Subject: [PATCH 171/771] Introduce eglot-handle-request and eglot-handle-notification as api * eglot.el (eglot--process-receive): Call eglot-handle-request and eglot-handle-notification. (eglot-handle-notification, eglot-handle-request): New generic functions. (eglot--server-window/showMessage) (eglot--server-window/progress) (eglot--server-telemetry/event, eglot--server-window/logMessage): Convert to eglot-handle-notification. (eglot-handle-request, eglot--server-client/registerCapability) (eglot--server-client/unregisterCapability) (eglot-handle-request): Convert to eglot-handle-request. --- lisp/progmodes/eglot.el | 66 +++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9d7253af26d..65beb35b2a8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -541,17 +541,21 @@ is a symbol saying if this is a client or server originated." (gethash id (eglot--pending-continuations proc))))) (eglot--log-event proc message 'server) (when error (setf (eglot--status proc) `(,error t))) - (cond (method - ;; a server notification or a server request - (let* ((handler-sym (intern (concat "eglot--server-" method)))) - (if (functionp handler-sym) - ;; FIXME: will fail if params is array instead of not an object - (apply handler-sym proc (append params (if id `(:id ,id)))) - (eglot--warn "No implementation of method %s yet" method) - (when id - (eglot--reply - proc id - :error `(:code -32601 :message "Method unimplemented")))))) + (unless (or (null method) + (keywordp method)) + (setq method (intern (format ":%s" method)))) + (cond ((and method id) + (condition-case-unless-debug _err + (apply #'eglot-handle-request proc id method params) + (cl-no-applicable-method + (eglot--reply proc id + :error `(:code -32601 :message "Method unimplemented"))))) + (method + (condition-case-unless-debug _err + (apply #'eglot-handle-notification proc method params) + (cl-no-applicable-method + (eglot--log-event + proc '(:error `(:message "Notification unimplemented")))))) (continuations (cancel-timer (cl-third continuations)) (remhash id (eglot--pending-continuations proc)) @@ -953,14 +957,15 @@ function with the server still running." (eglot--warn "Brutally deleting non-compliant existing process %s" proc) (delete-process proc)))) -(cl-defun eglot--server-window/showMessage (_process &key type message) +(cl-defmethod eglot-handle-notification + (_process (_method (eql :window/showMessage)) &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) -(cl-defun eglot--server-window/showMessageRequest - (process &key id type message actions) +(cl-defmethod eglot-handle-request + (process id (_method (eql :window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" (let (reply) (unwind-protect @@ -980,17 +985,19 @@ function with the server still running." :error (eglot--obj :code -32800 :message "User cancelled")))))) -(cl-defun eglot--server-window/logMessage (_proc &key _type _message) +(cl-defmethod eglot-handle-notification + (_proc (_method (eql :window/logMessage)) &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer -(cl-defun eglot--server-telemetry/event (_proc &rest _any) +(cl-defmethod eglot-handle-notification + (_proc (_method (eql :telemetry/event)) &rest _any) "Handle notification telemetry/event") ;; noop, use events buffer (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") -(cl-defun eglot--server-textDocument/publishDiagnostics - (_proc &key uri diagnostics) +(cl-defmethod eglot-handle-notification + (_proc (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1015,7 +1022,7 @@ function with the server still running." (eglot--warn "Diagnostics received for unvisited %s" uri))) (cl-defun eglot--register-unregister (proc jsonrpc-id things how) - "Helper for `eglot--server-client/registerCapability'. + "Helper for `registerCapability'. THINGS are either registrations or unregisterations." (dolist (thing (cl-coerce things 'list)) (cl-destructuring-bind (&key id method registerOptions) thing @@ -1030,18 +1037,19 @@ THINGS are either registrations or unregisterations." :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) (eglot--reply proc jsonrpc-id :result (eglot--obj :message "OK"))) -(cl-defun eglot--server-client/registerCapability - (proc &key id registrations) +(cl-defmethod eglot-handle-request + (proc id (_method (eql :client/registerCapability)) &key registrations) "Handle server request client/registerCapability" (eglot--register-unregister proc id registrations 'register)) -(cl-defun eglot--server-client/unregisterCapability - (proc &key id unregisterations) ;; XXX: Yeah, typo and all.. See spec... +(cl-defmethod eglot-handle-request + (proc id (_method (eql :client/unregisterCapability)) + &key unregisterations) ;; XXX: "unregisterations" (sic) "Handle server request client/unregisterCapability" (eglot--register-unregister proc id unregisterations 'unregister)) -(cl-defun eglot--server-workspace/applyEdit - (proc &key id _label edit) +(cl-defmethod eglot-handle-request + (proc id (_method (eql :workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) @@ -1563,12 +1571,12 @@ Proceed? " "Prepare `eglot' to deal with RLS's special treatment." (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) -(cl-defun eglot--server-window/progress - (process &key id done title message &allow-other-keys) +(cl-defmethod eglot-handle-notification + (proc (_method (eql :window/progress)) &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner process) (list id title done message)) + (setf (eglot--spinner proc) (list id title done message)) (when (and (equal "Indexing" title) done) - (dolist (buffer (eglot--managed-buffers process)) + (dolist (buffer (eglot--managed-buffers proc)) (with-current-buffer buffer (funcall (or eglot--current-flymake-report-fn #'ignore) eglot--unreported-diagnostics))))) From 7e1b0cd1331e659b18b40e32af4f57fc089d5354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 22 May 2018 02:00:49 +0100 Subject: [PATCH 172/771] Use an eieio class to represent a server. Allow clients of eglot.el to use specific server classes to represent experimental servers. Wherever you used to read "proc" you now probably read "server", unless it's really the process properties that are sought after. Should help Josh Elsasser implement pull request https://github.com/joaotavora/eglot/issues/6. * eglot-tests.el (eglot--call-with-dirs-and-files) (auto-detect-running-server, auto-reconnect, basic-completions) (hover-after-completions): Adapt to server defclass instead of proc. * eglot.el (eglot-server-programs): Add docstring. (eglot--processes-by-project): Removed. (eglot--servers-by-project): New variable. (eglot--current-process): Removed. (eglot--current-server): New function. (eglot-server): New class. (cl-print-object eglot-server): New method. (eglot--current-process-or-lose): Removed. (eglot--current-server-or-lose): New function. (eglot--define-process-var): Remove. (eglot--make-process): Rework. (eglot--project-short-name): Remove. (eglot--connect): Rework. (eglot--interactive): Rework to allow custom classes. (eglot, eglot-reconnect, eglot--process-sentinel) (eglot--process-filter, eglot-events-buffer, eglot--log-event): Rework. (eglot--process-receive): Removed. (eglot--server-receive): New function. (eglot--send): Renamed from eglot--process-send. (eglot--process-send): Removed. (eglot-forget-pending-continuations) (eglot-clear-status, eglot--call-deferred) (eglot--server-ready-p, eglot--async-request, eglot--request) (eglot--notify, eglot--reply, eglot--managed-mode-onoff) (eglot--maybe-activate-editing-mode, eglot--mode-line-format): Rework. (eglot-shutdown): Rework. (eglot-handle-notification *, eglot-handle-request *) (eglot--register-unregister) (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didClose) (eglot--signal-textDocument/willSave) (eglot--signal-textDocument/didSave) (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point) (eglot-help-at-point, eglot-eldoc-function, eglot-imenu) (eglot-rename) (eglot--register-workspace/didChangeWatchedFiles) (eglot--unregister-workspace/didChangeWatchedFiles) (eglot--rls-probably-ready-for-p, eglot-handle-notification): Rework (proc->server) fixup * eglot-tests.el (eglot--call-with-dirs-and-files) (auto-detect-running-server, auto-reconnect, basic-completions) (hover-after-completions): * eglot.el (eglot--processes-by-project): Removed. (eglot--servers-by-project): New variable. (eglot--current-process): Removed. (eglot--current-server): New function. --- lisp/progmodes/eglot.el | 758 +++++++++++++++++++++------------------- 1 file changed, 392 insertions(+), 366 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 65beb35b2a8..cb213e515c4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -74,7 +74,25 @@ (sh-mode . ("bash-language-server" "start")) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) - "Alist mapping major modes to server executables.") + "How the command `eglot' guesses the server to start. +An association list of (MAJOR-MODE . SPEC) pair. MAJOR-MODE is a +mode symbol. SPEC is + +* In the most common case, a list of strings (PROGRAM [ARGS...]). +PROGRAM is called with ARGS and is expected to serve LSP requests +over the standard input/output channels. + +* A list (HOST PORT [ARGS...]) where HOST is a string and PORT is a +positive integer number for connecting to a server via TCP. +Remaining ARGS are passed to `open-network-stream' for upgrading +the connection with encryption, etc... + +* A function of no arguments returning a connected process. + +* A cons (CLASS-NAME . SPEC) where CLASS-NAME is a symbol +designating a subclass of `eglot-lsp-server', for +representing experimental LSP servers. In this case SPEC is +interpreted as described above this point.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -96,93 +114,86 @@ lasted more than that many seconds." ;;; Process management -(defvar eglot--processes-by-project (make-hash-table :test #'equal) +(defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defun eglot--current-process () +(defclass eglot-lsp-server () + ((process + :documentation "Wrapped process object." + :initarg :process :accessor eglot--process) + (name + :documentation "Readable name used for naming processes, buffers, etc..." + :initarg :name :accessor eglot--name) + (project-nickname + :documentation "Short nickname for the associated project." + :initarg :project-nickname :accessor eglot--project-nickname) + (major-mode + :documentation "Major mode symbol." + :initarg :major-mode :accessor eglot--major-mode) + (pending-continuations + :documentation "Map request ID's to (SUCCESS-FN ERROR-FN TIMEOUT-FN) triads." + :initform (make-hash-table) :accessor eglot--pending-continuations) + (events-buffer + :documentation "Buffer holding a log of server-related events." + :accessor eglot--events-buffer) + (capabilities + :documentation "JSON object containing server capabilities." + :accessor eglot--capabilities) + (moribund + :documentation "Flag set when server is shutting down." + :accessor eglot--moribund) + (project + :documentation "Project associated with server." + :initarg :project :accessor eglot--project) + (spinner + :documentation "List (ID DOING-WHAT DONE-P) representing server progress." + :initform `(nil nil t) :accessor eglot--spinner) + (status + :documentation "List (STATUS SERIOUS-P) representing server problems/status." + :initform `(:unknown nil) :accessor eglot--status) + (inhibit-autoreconnect + :documentation "Generalized boolean inhibiting auto-reconnection if true." + :initarg :inhibit-autoreconnect :accessor eglot--inhibit-autoreconnect) + (contact + :documentation "How server was started and how it can be re-started." + :initarg :contact :accessor eglot--contact) + (deferred-actions + :documentation "Map (DEFERRED-ID BUF) to (FN TIMER). +DEFERRED request from BUF is FN. It's sent later, not later than TIMER." + :initform (make-hash-table :test #'equal) :accessor eglot--deferred-actions) + (file-watches + :documentation "Map ID to list of WATCHES for `didChangeWatchedFiles'." + :initform (make-hash-table :test #'equal) :accessor eglot--file-watches) + (managed-buffers + :documentation "List of buffers managed by server." + :initarg :managed-buffers :accessor eglot--managed-buffers)) + :documentation + "Represents a server. Wraps a process for LSP communication.") + +(cl-defmethod cl-print-object ((obj eglot-lsp-server) stream) + (princ (format "#<%s: %s>" (eieio-object-class obj) (eglot--name obj)) stream)) + +(defun eglot--current-server () "The current logical EGLOT process." (let* ((probe (or (project-current) `(transient . ,default-directory)))) - (cl-find major-mode (gethash probe eglot--processes-by-project) + (cl-find major-mode (gethash probe eglot--servers-by-project) :key #'eglot--major-mode))) -(defun eglot--current-process-or-lose () +(defun eglot--current-server-or-lose () "Return the current EGLOT process or error." - (or (eglot--current-process) (eglot--error "No current EGLOT process"))) + (or (eglot--current-server) (eglot--error "No current EGLOT process"))) -(defmacro eglot--define-process-var (var-sym initval &optional doc) - "Define VAR-SYM as a generalized process-local variable. -INITVAL is the default value. DOC is the documentation." - (declare (indent 2) (doc-string 3)) - `(progn - (defun ,var-sym (proc) - ,doc (let* ((plist (process-plist proc)) - (probe (plist-member plist ',var-sym))) - (if probe (cadr probe) - (let ((def ,initval)) (process-put proc ',var-sym def) def)))) - (gv-define-setter ,var-sym (to-store process) - `(let ((once ,to-store)) (process-put ,process ',',var-sym once) once)))) - -(eglot--define-process-var eglot--short-name nil - "A short name for the process") - -(eglot--define-process-var eglot--major-mode nil - "The major-mode this server is managing.") - -(eglot--define-process-var eglot--expected-bytes nil - "How many bytes declared by server") - -(eglot--define-process-var eglot--pending-continuations (make-hash-table) - "A hash table of request ID to continuation lambdas") - -(eglot--define-process-var eglot--events-buffer nil - "A buffer pretty-printing the EGLOT RPC events") - -(eglot--define-process-var eglot--capabilities :unreported - "Holds list of capabilities that server reported") - -(eglot--define-process-var eglot--moribund nil - "Non-nil if server is about to exit") - -(eglot--define-process-var eglot--project nil - "The project the server belongs to.") - -(eglot--define-process-var eglot--spinner `(nil nil t) - "\"Spinner\" used by some servers. -A list (ID WHAT DONE-P).") - -(eglot--define-process-var eglot--status `(:unknown nil) - "Status as declared by the server. -A list (WHAT SERIOUS-P).") - -(eglot--define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect - "If non-nil, don't autoreconnect on unexpected quit.") - -(eglot--define-process-var eglot--contact nil - "Method used to contact a server.") - -(eglot--define-process-var eglot--deferred-actions - (make-hash-table :test #'equal) - "Actions deferred to when server is thought to be ready.") - -(eglot--define-process-var eglot--file-watches (make-hash-table :test #'equal) - "File system watches for the didChangeWatchedfiles thingy.") - -(eglot--define-process-var eglot--managed-buffers nil - "Buffers managed by the server.") - -(defun eglot--make-process (name managed-major-mode contact) - "Make a process from CONTACT. +(defun eglot--make-process (name contact) + "Make a process object from CONTACT. NAME is used to name the the started process or connection. -MANAGED-MAJOR-MODE is a symbol naming a major mode. CONTACT is in `eglot'. Returns a process object." - (let* ((readable-name (format "EGLOT server (%s/%s)" name managed-major-mode)) - (buffer (get-buffer-create (format "*%s stdout*" readable-name))) + (let* ((buffer (get-buffer-create (format "*%s stdout*" name))) (proc (cond ((processp contact) contact) ((integerp (cadr contact)) - (apply #'open-network-stream readable-name buffer contact)) + (apply #'open-network-stream name buffer contact)) (t (make-process - :name readable-name + :name name :command contact :coding 'no-conversion :connection-type 'pipe @@ -191,6 +202,8 @@ CONTACT is in `eglot'. Returns a process object." (set-marker (process-mark proc) (with-current-buffer buffer (point-min))) (set-process-filter proc #'eglot--process-filter) (set-process-sentinel proc #'eglot--process-sentinel) + (with-current-buffer buffer + (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t))) proc)) (defmacro eglot--obj (&rest what) @@ -200,10 +213,6 @@ CONTACT is in `eglot'. Returns a process object." ;; the indenting of literal plists. `(list ,@what)) -(defun eglot--project-short-name (project) - "Give PROJECT a short name." - (file-name-base (directory-file-name (car (project-roots project))))) - (defun eglot--all-major-modes () "Return all know major modes." (let ((retval)) @@ -237,54 +246,57 @@ CONTACT is in `eglot'. Returns a process object." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--connect (project managed-major-mode short-name contact _interactive) - "Connect for PROJECT, MANAGED-MAJOR-MODE, SHORT-NAME and CONTACT. -INTERACTIVE is t if inside interactive call." - (let* ((proc (eglot--make-process - short-name managed-major-mode (if (functionp contact) - (funcall contact) contact))) - (buffer (process-buffer proc))) - (setf (eglot--contact proc) contact - (eglot--project proc) project - (eglot--major-mode proc) managed-major-mode) - (with-current-buffer buffer - (let ((inhibit-read-only t) success) - (setf (eglot--inhibit-autoreconnect proc) - (cond - ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) - ((cl-plusp eglot-autoreconnect) - (run-with-timer eglot-autoreconnect nil - (lambda () - (setf (eglot--inhibit-autoreconnect proc) - (null eglot-autoreconnect))))))) - (setf (eglot--short-name proc) short-name) - (push proc (gethash project eglot--processes-by-project)) - (run-hook-with-args 'eglot-connect-hook proc) - (erase-buffer) - (read-only-mode t) - (unwind-protect - (cl-destructuring-bind (&key capabilities) - (eglot--request - proc - :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :capabilities(eglot--client-capabilities) - :rootPath (expand-file-name - (car (project-roots project))) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [])) - (setf (eglot--capabilities proc) capabilities) - (setf (eglot--status proc) nil) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode proc))) - (eglot--notify proc :initialized (eglot--obj :__dummy__ t)) - (setq success proc)) - (unless (or success (not (process-live-p proc)) (eglot--moribund proc)) - (eglot-shutdown proc))))))) +(defun eglot--connect (project managed-major-mode contact server-class) + "Connect for PROJECT, MANAGED-MAJOR-MODE and CONTACT. +INTERACTIVE is t if inside interactive call. Return an object of +class SERVER-CLASS." + (let* ((nickname (file-name-base (directory-file-name + (car (project-roots project))))) + (name (format "EGLOT (%s/%s)" nickname managed-major-mode)) + (proc (eglot--make-process + name (if (functionp contact) (funcall contact) contact))) + server connect-success) + (setq server + (make-instance + (or server-class 'eglot-lsp-server) + :process proc :major-mode managed-major-mode + :project project :contact contact + :name name :project-nickname nickname + :inhibit-autoreconnect + (cond + ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) + ((cl-plusp eglot-autoreconnect) + (run-with-timer eglot-autoreconnect nil + (lambda () + (setf (eglot--inhibit-autoreconnect server) + (null eglot-autoreconnect)))))))) + (push server (gethash project eglot--servers-by-project)) + (process-put proc 'eglot-server server) + (unwind-protect + (cl-destructuring-bind (&key capabilities) + (eglot--request + server + :initialize + (eglot--obj :processId (unless (eq (process-type proc) + 'network) + (emacs-pid)) + :capabilities(eglot--client-capabilities) + :rootPath (expand-file-name + (car (project-roots project))) + :rootUri (eglot--path-to-uri + (car (project-roots project))) + :initializationOptions [])) + (setf (eglot--capabilities server) capabilities) + (setf (eglot--status server) nil) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode server))) + (eglot--notify server :initialized (eglot--obj :__dummy__ t)) + (run-hook-with-args 'eglot-connect-hook server) + (setq connect-success server)) + (unless (or connect-success + (not (process-live-p proc)) (eglot--moribund server)) + (eglot-shutdown server))))) (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") @@ -303,34 +315,37 @@ INTERACTIVE is t if inside interactive call." (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) (t guessed-mode))) (project (or (project-current) `(transient . ,default-directory))) - (guessed (cdr (assoc managed-mode eglot-server-programs))) - (program (and (listp guessed) (stringp (car guessed)) (car guessed))) + (guess (cdr (assoc managed-mode eglot-server-programs))) + (class (and (consp guess) (symbolp (car guess)) + (prog1 (car guess) (setq guess (cdr guess))))) + (program (and (listp guess) (stringp (car guess)) (car guess))) (base-prompt "[eglot] Enter program to execute (or :): ") (prompt (cond (current-prefix-arg base-prompt) - ((null guessed) + ((null guess) (format "[eglot] Sorry, couldn't guess for `%s'\n%s!" managed-mode base-prompt)) ((and program (not (executable-find program))) (concat (format "[eglot] I guess you want to run `%s'" - (combine-and-quote-strings guessed)) + (combine-and-quote-strings guess)) (format ", but I can't find `%s' in PATH!" program) "\n" base-prompt)))) (contact (if prompt (let ((s (read-shell-command prompt - (if program (combine-and-quote-strings guessed)) + (if program (combine-and-quote-strings guess)) 'eglot-command-history))) (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" (string-trim s)) (list (match-string 1 s) (string-to-number (match-string 2 s))) (split-string-and-unquote s))) - guessed))) - (list managed-mode project contact t))) + guess))) + (list managed-mode project contact class t))) ;;;###autoload -(defun eglot (managed-major-mode project command &optional interactive) +(defun eglot (managed-major-mode project command server-class + &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. The LSP server is started (or contacted) via COMMAND. If this @@ -356,86 +371,90 @@ list is of the form \":\" it is taken as an indication to connect to a server instead of starting one. This is also know as the server's \"contact\". -MANAGED-MAJOR-MODE is an Emacs major mode. +SERVER-CLASS is a symbol naming a class that must inherit from +`eglot-server', or nil to use the default server class. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) - (let* ((short-name (eglot--project-short-name project))) - (let ((current-process (eglot--current-process))) - (if (and (process-live-p current-process) - interactive - (y-or-n-p "[eglot] Live process found, reconnect instead? ")) - (eglot-reconnect current-process interactive) - (when (process-live-p current-process) - (eglot-shutdown current-process)) - (let ((proc (eglot--connect project + (let ((current-server (eglot--current-server))) + (if (and current-server + (process-live-p (eglot--process current-server)) + interactive + (y-or-n-p "[eglot] Live process found, reconnect instead? ")) + (eglot-reconnect current-server interactive) + (when (and current-server + (process-live-p (eglot--process current-server))) + (eglot-shutdown current-server)) + (let ((server (eglot--connect project managed-major-mode - short-name command - interactive))) - (eglot--message "Connected! Process `%s' now \ + server-class))) + (eglot--message "Connected! Server `%s' now \ managing `%s' buffers in project `%s'." - proc managed-major-mode short-name) - proc))))) + (eglot--name server) managed-major-mode + (eglot--project-nickname server)) + server)))) -(defun eglot-reconnect (process &optional interactive) - "Reconnect to PROCESS. +(defun eglot-reconnect (server &optional interactive) + "Reconnect to SERVER. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (when (process-live-p process) - (eglot-shutdown process interactive)) - (eglot--connect (eglot--project process) - (eglot--major-mode process) - (eglot--short-name process) - (eglot--contact process) - interactive) + (interactive (list (eglot--current-server-or-lose) t)) + (when (process-live-p (eglot--process server)) + (eglot-shutdown server interactive)) + (eglot--connect (eglot--project server) + (eglot--major-mode server) + (eglot--contact server) + (eieio-object-class server)) (eglot--message "Reconnected!")) (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." - (eglot--log-event proc `(:message "Process state changed" :change ,change)) - (when (not (process-live-p proc)) - (with-current-buffer (eglot-events-buffer proc) - (let ((inhibit-read-only t)) - (insert "\n----------b---y---e---b---y---e----------\n"))) - ;; Cancel outstanding timers and file system watches - (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success _error timeout) triplet - (cancel-timer timeout))) - (eglot--pending-continuations proc)) - (maphash (lambda (_id watches) - (mapcar #'file-notify-rm-watch watches)) - (eglot--file-watches proc)) - (unwind-protect - ;; Call all outstanding error handlers - (maphash (lambda (_id triplet) - (cl-destructuring-bind (_success error _timeout) triplet - (funcall error `(:code -1 :message "Server died")))) - (eglot--pending-continuations proc)) - ;; Turn off `eglot--managed-mode' where appropriate. - (dolist (buffer (eglot--managed-buffers proc)) - (with-current-buffer buffer (eglot--managed-mode-onoff proc -1))) - ;; Forget about the process-project relationship - (setf (gethash (eglot--project proc) eglot--processes-by-project) - (delq proc - (gethash (eglot--project proc) eglot--processes-by-project))) - ;; Say last words - (eglot--message "%s exited with status %s" proc (process-exit-status proc)) - (delete-process proc) - ;; Consider autoreconnecting - (cond ((eglot--moribund proc)) - ((not (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Reconnecting after unexpected server exit") - (eglot-reconnect proc)) - ((timerp (eglot--inhibit-autoreconnect proc)) - (eglot--warn "Not auto-reconnecting, last on didn't last long.")))))) + (let ((server (process-get proc 'eglot-server))) + (eglot--log-event server `(:message "Process state changed" :change ,change)) + (when (not (process-live-p proc)) + (with-current-buffer (eglot-events-buffer server) + (let ((inhibit-read-only t)) + (insert "\n----------b---y---e---b---y---e----------\n"))) + ;; Cancel outstanding timers and file system watches + (maphash (lambda (_id triplet) + (cl-destructuring-bind (_success _error timeout) triplet + (cancel-timer timeout))) + (eglot--pending-continuations server)) + (maphash (lambda (_id watches) + (mapcar #'file-notify-rm-watch watches)) + (eglot--file-watches server)) + (unwind-protect + ;; Call all outstanding error handlers + (maphash (lambda (_id triplet) + (cl-destructuring-bind (_success error _timeout) triplet + (funcall error `(:code -1 :message "Server died")))) + (eglot--pending-continuations server)) + ;; Turn off `eglot--managed-mode' where appropriate. + (dolist (buffer (eglot--managed-buffers server)) + (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) + ;; Forget about the process-project relationship + (setf (gethash (eglot--project server) eglot--servers-by-project) + (delq server + (gethash (eglot--project server) eglot--servers-by-project))) + ;; Say last words + (eglot--message "%s exited with status %s" (eglot--name server) + (process-exit-status + (eglot--process server))) + (delete-process proc) + ;; Consider autoreconnecting + (cond ((eglot--moribund server)) + ((not (eglot--inhibit-autoreconnect server)) + (eglot--warn "Reconnecting after unexpected server exit") + (eglot-reconnect server)) + ((timerp (eglot--inhibit-autoreconnect server)) + (eglot--warn "Not auto-reconnecting, last on didn't last long."))))))) (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." (when (buffer-live-p (process-buffer proc)) (with-current-buffer (process-buffer proc) (let ((inhibit-read-only t) - (expected-bytes (eglot--expected-bytes proc))) + (expected-bytes (process-get proc 'eglot-expected-bytes))) ;; Insert the text, advancing the process marker. ;; (save-excursion @@ -480,7 +499,9 @@ INTERACTIVE is t if called interactively." ;; shielding buffer from tamper ;; (with-temp-buffer - (eglot--process-receive proc json-message)))) + (eglot--server-receive + (process-get proc 'eglot-server) + json-message)))) (goto-char message-end) (delete-region (point-min) (point)) (setq expected-bytes nil)))) @@ -490,31 +511,31 @@ INTERACTIVE is t if called interactively." (setq done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; - (setf (eglot--expected-bytes proc) expected-bytes)))))) + (process-put proc 'eglot-expected-bytes expected-bytes)))))) -(defun eglot-events-buffer (process &optional interactive) - "Display events buffer for current LSP connection PROCESS. +(defun eglot-events-buffer (server &optional interactive) + "Display events buffer for current LSP SERVER. INTERACTIVE is t if called interactively." - (interactive (list (eglot--current-process-or-lose) t)) - (let* ((probe (eglot--events-buffer process)) - (buffer (or (and (buffer-live-p probe) - probe) + (interactive (list (eglot--current-server-or-lose) t)) + (let* ((probe (eglot--events-buffer server)) + (buffer (or (and (buffer-live-p probe) probe) (let ((buffer (get-buffer-create (format "*%s events*" - (process-name process))))) + (eglot--name server))))) (with-current-buffer buffer (buffer-disable-undo) (read-only-mode t) - (setf (eglot--events-buffer process) buffer)) + (setf (eglot--events-buffer server) buffer)) buffer)))) (when interactive (display-buffer buffer)) buffer)) -(defun eglot--log-event (proc message &optional type) +(defun eglot--log-event (server message &optional type) "Log an eglot-related event. -PROC is the current process. MESSAGE is a JSON-like plist. TYPE -is a symbol saying if this is a client or server originated." - (with-current-buffer (eglot-events-buffer proc) +SERVER is the current server. MESSAGE is a JSON-like plist. +TYPE is a symbol saying if this is a client or server +originated." + (with-current-buffer (eglot-events-buffer server) (cl-destructuring-bind (&key method id error &allow-other-keys) message (let* ((inhibit-read-only t) (subtype (cond ((and method id) 'request) @@ -533,47 +554,47 @@ is a symbol saying if this is a client or server originated." (setq msg (propertize msg 'face 'error))) (insert-before-markers msg)))))) -(defun eglot--process-receive (proc message) - "Process MESSAGE from PROC." +(defun eglot--server-receive (server message) + "Process MESSAGE from SERVER." (cl-destructuring-bind (&key method id params error result _jsonrpc) message (let* ((continuations (and id (not method) - (gethash id (eglot--pending-continuations proc))))) - (eglot--log-event proc message 'server) - (when error (setf (eglot--status proc) `(,error t))) + (gethash id (eglot--pending-continuations server))))) + (eglot--log-event server message 'server) + (when error (setf (eglot--status server) `(,error t))) (unless (or (null method) (keywordp method)) (setq method (intern (format ":%s" method)))) (cond ((and method id) (condition-case-unless-debug _err - (apply #'eglot-handle-request proc id method params) + (apply #'eglot-handle-request server id method params) (cl-no-applicable-method - (eglot--reply proc id - :error `(:code -32601 :message "Method unimplemented"))))) + (eglot--reply server id + :error `(:code -32601 :message "Method unimplemented"))))) (method (condition-case-unless-debug _err - (apply #'eglot-handle-notification proc method params) + (apply #'eglot-handle-notification server method params) (cl-no-applicable-method (eglot--log-event - proc '(:error `(:message "Notification unimplemented")))))) + server '(:error `(:message "Notification unimplemented")))))) (continuations (cancel-timer (cl-third continuations)) - (remhash id (eglot--pending-continuations proc)) + (remhash id (eglot--pending-continuations server)) (if error (funcall (cl-second continuations) error) (funcall (cl-first continuations) result))) (id (eglot--warn "Ooops no continuation for id %s" id))) - (eglot--call-deferred proc) + (eglot--call-deferred server) (force-mode-line-update t)))) -(defun eglot--process-send (proc message) - "Send MESSAGE to PROC (ID is optional)." +(defun eglot--send (server message) + "Send MESSAGE to SERVER (ID is optional)." (let ((json (json-encode message))) - (process-send-string proc (format "Content-Length: %d\r\n\r\n%s" - (string-bytes json) - json)) - (eglot--log-event proc message 'client))) + (process-send-string (eglot--process server) + (format "Content-Length: %d\r\n\r\n%s" + (string-bytes json) json)) + (eglot--log-event server message 'client))) (defvar eglot--next-request-id 0 "ID for next request.") @@ -581,21 +602,21 @@ is a symbol saying if this is a client or server originated." "Compute the next id for a client request." (setq eglot--next-request-id (1+ eglot--next-request-id))) -(defun eglot-forget-pending-continuations (process) - "Stop waiting for responses from the current LSP PROCESS." - (interactive (list (eglot--current-process-or-lose))) - (clrhash (eglot--pending-continuations process))) +(defun eglot-forget-pending-continuations (server) + "Stop waiting for responses from the current LSP SERVER." + (interactive (list (eglot--current-server-or-lose))) + (clrhash (eglot--pending-continuations server))) -(defun eglot-clear-status (process) - "Clear most recent error message from PROCESS." - (interactive (list (eglot--current-process-or-lose))) - (setf (eglot--status process) nil) +(defun eglot-clear-status (server) + "Clear most recent error message from SERVER." + (interactive (list (eglot--current-server-or-lose))) + (setf (eglot--status server) nil) (force-mode-line-update t)) -(defun eglot--call-deferred (proc) - "Call PROC's deferred actions, who may again defer themselves." - (when-let ((actions (hash-table-values (eglot--deferred-actions proc)))) - (eglot--log-event proc `(:running-deferred ,(length actions))) +(defun eglot--call-deferred (server) + "Call SERVER's deferred actions, who may again defer themselves." + (when-let ((actions (hash-table-values (eglot--deferred-actions server)))) + (eglot--log-event server `(:running-deferred ,(length actions))) (mapc #'funcall (mapcar #'car actions)))) (defvar eglot--ready-predicates '(eglot--server-ready-p) @@ -604,8 +625,8 @@ If one of these returns nil, a deferrable `eglot--async-request' will be deferred. Each predicate is passed the symbol for the request request and a process object.") -(defun eglot--server-ready-p (_what _proc) - "Tell if server of PROC ready for processing deferred WHAT." +(defun eglot--server-ready-p (_what _server) + "Tell if SERVER is ready for processing deferred WHAT." (not (eglot--outstanding-edits-p))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) @@ -613,14 +634,14 @@ request request and a process object.") (let ((e (gensym "eglot--lambda-elem"))) `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) -(cl-defun eglot--async-request (proc +(cl-defun eglot--async-request (server method params &rest args &key success-fn error-fn timeout-fn (timeout eglot-request-timeout) (deferred nil)) - "Make a request to PROCESS, expecting a reply later on. + "Make a request to SERVER expecting a reply later on. SUCCESS-FN and ERROR-FN are passed `:result' and `:error' objects, respectively. Wait TIMEOUT seconds for response or call nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the @@ -636,52 +657,55 @@ TIMER)." (run-with-timer timeout nil (lambda () - (remhash id (eglot--pending-continuations proc)) + (remhash id (eglot--pending-continuations server)) (funcall (or timeout-fn (lambda () (eglot--log-event - proc `(:timed-out ,method :id ,id - :params ,params))))))))))) + server `(:timed-out ,method :id ,id + :params ,params))))))))))) (when deferred (let* ((buf (current-buffer)) - (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) + (existing (gethash (list deferred buf) + (eglot--deferred-actions server)))) (when existing (setq existing (cadr existing))) (if (run-hook-with-args-until-failure 'eglot--ready-predicates - deferred proc) - (remhash (list deferred buf) (eglot--deferred-actions proc)) - (eglot--log-event proc `(:deferring ,method :id ,id :params ,params)) + deferred server) + (remhash (list deferred buf) (eglot--deferred-actions server)) + (eglot--log-event server `(:deferring ,method :id ,id :params ,params)) (let* ((buf (current-buffer)) (point (point)) (later (lambda () (when (buffer-live-p buf) (with-current-buffer buf - (save-excursion (goto-char point) - (apply #'eglot--async-request proc - method params args))))))) - (puthash (list deferred buf) (list later (setq timer (funcall make-timer))) - (eglot--deferred-actions proc)) + (save-excursion + (goto-char point) + (apply #'eglot--async-request server + method params args))))))) + (puthash (list deferred buf) + (list later (setq timer (funcall make-timer))) + (eglot--deferred-actions server)) (cl-return-from eglot--async-request nil))))) ;; Really run it ;; - (eglot--process-send proc (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)) + (eglot--send server (eglot--obj :jsonrpc "2.0" + :id id + :method method + :params params)) (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) (eglot--log-event - proc (eglot--obj :message "success ignored" :id id)))) + server (eglot--obj :message "success ignored" :id id)))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status proc) `(,message t)) - proc (eglot--obj :message "error ignored, status set" - :id id :error code))) + (setf (eglot--status server) `(,message t)) + server (eglot--obj :message "error ignored, status set" + :id id :error code))) (setq timer (funcall make-timer))) - (eglot--pending-continuations proc)) + (eglot--pending-continuations server)) (list id timer))) -(defun eglot--request (proc method params &optional deferred) - "Like `eglot--async-request' for PROC, METHOD and PARAMS, but synchronous. +(defun eglot--request (server method params &optional deferred) + "Like `eglot--async-request' for SERVER, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally. DEFERRED is passed to `eglot--async-request', which see." ;; Launching a deferred sync request with outstanding changes is a @@ -695,7 +719,7 @@ DEFERRED is passed to `eglot--async-request', which see." (setq id-and-timer (eglot--async-request - proc method params + server method params :success-fn (lambda (result) (throw done `(done ,result))) :timeout-fn (lambda () (throw done '(error "Timed out"))) :error-fn (eglot--lambda (&key code message _data) @@ -704,23 +728,23 @@ DEFERRED is passed to `eglot--async-request', which see." :deferred deferred)) (while t (accept-process-output nil 30))) (pcase-let ((`(,id ,timer) id-and-timer)) - (when id (remhash id (eglot--pending-continuations proc))) + (when id (remhash id (eglot--pending-continuations server))) (when timer (cancel-timer timer)))))) (when (eq 'error (car res)) (eglot--error (cadr res))) (cadr res))) -(cl-defun eglot--notify (process method params) - "Notify PROCESS of something, don't expect a reply.e" - (eglot--process-send process (eglot--obj :jsonrpc "2.0" - :method method - :params params))) +(cl-defun eglot--notify (server method params) + "Notify SERVER of something, don't expect a reply.e" + (eglot--send server (eglot--obj :jsonrpc "2.0" + :method method + :params params))) -(cl-defun eglot--reply (process id &key result error) +(cl-defun eglot--reply (server id &key result error) "Reply to PROCESS's request ID with MESSAGE." - (eglot--process-send - process `(:jsonrpc "2.0" :id ,id - ,@(when result `(:result ,result)) - ,@(when error `(:error ,error))))) + (eglot--send + server`(:jsonrpc "2.0" :id ,id + ,@(when result `(:result ,result)) + ,@(when error `(:error ,error))))) ;;; Helpers @@ -788,7 +812,7 @@ If optional MARKER, return a marker instead" (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." -(cl-loop for caps = (eglot--capabilities (eglot--current-process-or-lose)) +(cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) then (cadr probe) for feat in feats for probe = (plist-member caps feat) @@ -839,14 +863,14 @@ If optional MARKERS, make markers." #'eglot-eldoc-function) (remove-function (local imenu-create-index-function) #'eglot-imenu)))) -(defun eglot--managed-mode-onoff (proc arg) - "Proxy for function `eglot--managed-mode' with ARG and PROC." +(defun eglot--managed-mode-onoff (server arg) + "Proxy for function `eglot--managed-mode' with ARG and SERVER." (eglot--managed-mode arg) (let ((buf (current-buffer))) (if eglot--managed-mode - (cl-pushnew buf (eglot--managed-buffers proc)) - (setf (eglot--managed-buffers proc) - (delq buf (eglot--managed-buffers proc)))))) + (cl-pushnew buf (eglot--managed-buffers server)) + (setf (eglot--managed-buffers server) + (delq buf (eglot--managed-buffers server)))))) (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) @@ -854,15 +878,15 @@ If optional MARKERS, make markers." (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") -(defun eglot--maybe-activate-editing-mode (&optional proc) +(defun eglot--maybe-activate-editing-mode (&optional server) "Maybe activate mode function `eglot--managed-mode'. -If PROC is supplied, do it only if BUFFER is managed by it. In +If SERVER is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." ;; Called even when revert-buffer-in-progress-p - (let* ((cur (and buffer-file-name (eglot--current-process))) - (proc (or (and (null proc) cur) (and proc (eq proc cur) cur)))) - (when proc - (eglot--managed-mode-onoff proc 1) + (let* ((cur (and buffer-file-name (eglot--current-server))) + (server (or (and (null server) cur) (and server (eq server cur) cur)))) + (when server + (eglot--managed-mode-onoff server 1) (eglot--signal-textDocument/didOpen) (flymake-start) (funcall (or eglot--current-flymake-report-fn #'ignore) nil)))) @@ -899,12 +923,14 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((proc (eglot--current-process)) - (name (and (process-live-p proc) (eglot--short-name proc))) - (pending (and proc (hash-table-count - (eglot--pending-continuations proc)))) - (`(,_id ,doing ,done-p ,detail) (and proc (eglot--spinner proc))) - (`(,status ,serious-p) (and proc (eglot--status proc)))) + (pcase-let* ((server (eglot--current-server)) + (name (and + server + (eglot--project-nickname server))) + (pending (and server (hash-table-count + (eglot--pending-continuations server)))) + (`(,_id ,doing ,done-p ,detail) (and server (eglot--spinner server))) + (`(,status ,serious-p) (and server (eglot--status server)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) (when name @@ -938,34 +964,34 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (proc &optional _interactive) - "Politely ask the server PROC to quit. +(defun eglot-shutdown (server &optional _interactive) + "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond. Don't leave this function with the server still running." - (interactive (list (eglot--current-process-or-lose) t)) - (eglot--message "Asking %s politely to terminate" proc) + (interactive (list (eglot--current-server-or-lose) t)) + (eglot--message "Asking %s politely to terminate" (eglot--name server)) (unwind-protect (let ((eglot-request-timeout 3)) - (setf (eglot--moribund proc) t) - (eglot--request proc :shutdown nil) + (setf (eglot--moribund server) t) + (eglot--request server :shutdown nil) ;; this one is supposed to always fail, hence ignore-errors - (ignore-errors (eglot--request proc :exit nil))) + (ignore-errors (eglot--request server :exit nil))) ;; Turn off `eglot--managed-mode' where appropriate. - (dolist (buffer (eglot--managed-buffers proc)) - (with-current-buffer buffer (eglot--managed-mode-onoff proc -1))) - (when (process-live-p proc) - (eglot--warn "Brutally deleting non-compliant existing process %s" proc) - (delete-process proc)))) + (dolist (buffer (eglot--managed-buffers server)) + (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) + (when (process-live-p (eglot--process server)) + (eglot--warn "Brutally deleting non-compliant server %s" (eglot--name server)) + (delete-process (eglot--process server))))) (cl-defmethod eglot-handle-notification - (_process (_method (eql :window/showMessage)) &key type message) + (_server (_method (eql :window/showMessage)) &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) (cl-defmethod eglot-handle-request - (process id (_method (eql :window/showMessageRequest)) &key type message actions) + (server id (_method (eql :window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" (let (reply) (unwind-protect @@ -980,24 +1006,24 @@ function with the server still running." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (eglot--reply process id :result (eglot--obj :title reply)) - (eglot--reply process id + (eglot--reply server id :result (eglot--obj :title reply)) + (eglot--reply server id :error (eglot--obj :code -32800 :message "User cancelled")))))) (cl-defmethod eglot-handle-notification - (_proc (_method (eql :window/logMessage)) &key _type _message) + (_server (_method (eql :window/logMessage)) &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer (cl-defmethod eglot-handle-notification - (_proc (_method (eql :telemetry/event)) &rest _any) + (_server (_method (eql :telemetry/event)) &rest _any) "Handle notification telemetry/event") ;; noop, use events buffer (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") (cl-defmethod eglot-handle-notification - (_proc (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) + (_server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1021,7 +1047,7 @@ function with the server still running." (setq eglot--unreported-diagnostics diags))))) (eglot--warn "Diagnostics received for unvisited %s" uri))) -(cl-defun eglot--register-unregister (proc jsonrpc-id things how) +(cl-defun eglot--register-unregister (server jsonrpc-id things how) "Helper for `registerCapability'. THINGS are either registrations or unregisterations." (dolist (thing (cl-coerce things 'list)) @@ -1029,32 +1055,32 @@ THINGS are either registrations or unregisterations." (let (retval) (unwind-protect (setq retval (apply (intern (format "eglot--%s-%s" how method)) - proc :id id registerOptions)) + server :id id registerOptions)) (unless (eq t (car retval)) (cl-return-from eglot--register-unregister (eglot--reply - proc jsonrpc-id + server jsonrpc-id :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (eglot--reply proc jsonrpc-id :result (eglot--obj :message "OK"))) + (eglot--reply server jsonrpc-id :result (eglot--obj :message "OK"))) (cl-defmethod eglot-handle-request - (proc id (_method (eql :client/registerCapability)) &key registrations) + (server id (_method (eql :client/registerCapability)) &key registrations) "Handle server request client/registerCapability" - (eglot--register-unregister proc id registrations 'register)) + (eglot--register-unregister server id registrations 'register)) (cl-defmethod eglot-handle-request - (proc id (_method (eql :client/unregisterCapability)) + (server id (_method (eql :client/unregisterCapability)) &key unregisterations) ;; XXX: "unregisterations" (sic) "Handle server request client/unregisterCapability" - (eglot--register-unregister proc id unregisterations 'unregister)) + (eglot--register-unregister server id unregisterations 'unregister)) (cl-defmethod eglot-handle-request - (proc id (_method (eql :workspace/applyEdit)) &key _label edit) + (server id (_method (eql :workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) - (eglot--reply proc id :result `(:applied ))) - (error (eglot--reply proc id + (eglot--reply server id :result `(:applied ))) + (error (eglot--reply server id :result `(:applied :json-false) :error (eglot--obj :code -32001 :message (format "%s" err)))))) @@ -1119,7 +1145,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when (eglot--outstanding-edits-p) - (let* ((proc (eglot--current-process-or-lose)) + (let* ((server (eglot--current-server-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) (emacs-messup (/= (length (car eglot--recent-changes)) (length (cdr eglot--recent-changes)))) @@ -1129,7 +1155,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (save-restriction (widen) (eglot--notify - proc :textDocument/didChange + server :textDocument/didChange (eglot--obj :textDocument (eglot--VersionedTextDocumentIdentifier) @@ -1145,38 +1171,38 @@ Records START, END and PRE-CHANGE-LENGTH locally." :rangeLength len :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) - (setf (eglot--spinner proc) (list nil :textDocument/didChange t)) - (eglot--call-deferred proc)))) + (setf (eglot--spinner server) (list nil :textDocument/didChange t)) + (eglot--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." (setq eglot--recent-changes (cons [] [])) (eglot--notify - (eglot--current-process-or-lose) + (eglot--current-server-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." (eglot--notify - (eglot--current-process-or-lose) + (eglot--current-server-or-lose) :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (let ((proc (eglot--current-process-or-lose)) + (let ((server (eglot--current-server-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) - (eglot--notify proc :textDocument/willSave params) + (eglot--notify server :textDocument/willSave params) (ignore-errors (let ((eglot-request-timeout 0.5)) (when (plist-get :willSaveWaitUntil (eglot--server-capable :textDocumentSync)) (eglot--apply-text-edits - (eglot--request proc :textDocument/willSaveWaituntil params))))))) + (eglot--request server :textDocument/willSaveWaituntil params))))))) (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." (eglot--notify - (eglot--current-process-or-lose) + (eglot--current-server-or-lose) :textDocument/didSave (eglot--obj ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. @@ -1218,7 +1244,7 @@ DUMMY is ignored" (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) - (let ((proc (eglot--current-process-or-lose)) + (let ((server (eglot--current-server-or-lose)) (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) @@ -1234,7 +1260,7 @@ DUMMY is ignored" :locations (list location) :kind kind :containerName containerName)) - (eglot--request proc + (eglot--request server :textDocument/documentSymbol (eglot--obj :textDocument text-id)))) @@ -1252,7 +1278,7 @@ DUMMY is ignored" (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) @@ -1271,7 +1297,7 @@ DUMMY is ignored" (eglot--error "Don' know where %s is in the workspace!" identifier)) (mapcar (eglot--lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :textDocument/references (append params @@ -1283,21 +1309,21 @@ DUMMY is ignored" (mapcar (eglot--lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :workspace/symbol (eglot--obj :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (proc (eglot--current-process-or-lose))) + (server (eglot--current-server-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) (or (cdr bounds) (point)) (completion-table-with-cache (lambda (_ignored) - (let* ((resp (eglot--request proc + (let* ((resp (eglot--request server :textDocument/completion (eglot--TextDocumentPositionParams) :textDocument/completion)) @@ -1328,7 +1354,7 @@ DUMMY is ignored" (or (get-text-property 0 :documentation obj) (and (eglot--server-capable :completionProvider :resolveProvider) - (plist-get (eglot--request proc :completionItem/resolve + (plist-get (eglot--request server :completionItem/resolve (text-properties-at 0 obj)) :documentation))))) (when documentation @@ -1374,7 +1400,7 @@ DUMMY is ignored" "Request \"hover\" information for the thing at point." (interactive) (cl-destructuring-bind (&key contents range) - (eglot--request (eglot--current-process-or-lose) :textDocument/hover + (eglot--request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) (with-help-window "*eglot help*" @@ -1385,7 +1411,7 @@ DUMMY is ignored" "EGLOT's `eldoc-documentation-function' function. If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((buffer (current-buffer)) - (proc (eglot--current-process-or-lose)) + (server (eglot--current-server-or-lose)) (position-params (eglot--TextDocumentPositionParams)) sig-showing) (cl-macrolet ((when-buffer-window @@ -1393,7 +1419,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (with-current-buffer buffer ,@body)))) (when (eglot--server-capable :signatureHelpProvider) (eglot--async-request - proc :textDocument/signatureHelp position-params + server :textDocument/signatureHelp position-params :success-fn (eglot--lambda (&key signatures activeSignature activeParameter) (when-buffer-window @@ -1405,7 +1431,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (eglot--async-request - proc :textDocument/hover position-params + server :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) (unless sig-showing (setq eldoc-last-message (eglot--hover-info contents range)) @@ -1413,7 +1439,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request - proc :textDocument/documentHighlight position-params + server :textDocument/documentHighlight position-params :success-fn (lambda (highlights) (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights @@ -1438,7 +1464,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :textDocument/documentSymbol (eglot--obj :textDocument (eglot--TextDocumentIdentifier)))))) @@ -1506,7 +1532,7 @@ Proceed? " (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (eglot--request (eglot--current-process-or-lose) + (eglot--request (eglot--current-server-or-lose) :textDocument/rename `(,@(eglot--TextDocumentPositionParams) ,@(eglot--obj :newName newname))) current-prefix-arg)) @@ -1514,9 +1540,9 @@ Proceed? " ;;; Dynamic registration ;;; -(cl-defun eglot--register-workspace/didChangeWatchedFiles (proc &key id watchers) +(cl-defun eglot--register-workspace/didChangeWatchedFiles (server &key id watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" - (eglot--unregister-workspace/didChangeWatchedFiles proc :id id) + (eglot--unregister-workspace/didChangeWatchedFiles server :id id) (let* (success (globs (mapcar (lambda (w) (plist-get w :globPattern)) watchers))) (cl-labels @@ -1531,7 +1557,7 @@ Proceed? " (expand-file-name glob)) f)))) (eglot--notify - proc :workspace/didChangeWatchedFiles + server :workspace/didChangeWatchedFiles `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) :type ,(cl-case action (created 1) @@ -1543,25 +1569,25 @@ Proceed? " (unwind-protect (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) (push (file-notify-add-watch dir '(change) #'handle-event) - (gethash id (eglot--file-watches proc)))) + (gethash id (eglot--file-watches server)))) (setq success `(t "OK"))) (unless success - (eglot--unregister-workspace/didChangeWatchedFiles proc :id id)))))) + (eglot--unregister-workspace/didChangeWatchedFiles server :id id)))))) -(cl-defun eglot--unregister-workspace/didChangeWatchedFiles (proc &key id) +(cl-defun eglot--unregister-workspace/didChangeWatchedFiles (server &key id) "Handle dynamic unregistration of workspace/didChangeWatchedFiles" - (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches proc))) - (remhash id (eglot--file-watches proc)) + (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches server))) + (remhash id (eglot--file-watches server)) (list t "OK")) ;;; Rust-specific ;;; -(defun eglot--rls-probably-ready-for-p (what proc) - "Guess if the RLS running in PROC is ready for WHAT." +(defun eglot--rls-probably-ready-for-p (what server) + "Guess if the RLS running in SERVER is ready for WHAT." (or (eq what :textDocument/completion) ; RLS normally ready for this ; one, even if building ; - (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner proc))) + (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner server))) (and (equal "Indexing" what) done)))) ;;;###autoload @@ -1572,11 +1598,11 @@ Proceed? " (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) (cl-defmethod eglot-handle-notification - (proc (_method (eql :window/progress)) &key id done title message &allow-other-keys) + (server (_method (eql :window/progress)) &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner proc) (list id title done message)) + (setf (eglot--spinner server) (list id title done message)) (when (and (equal "Indexing" title) done) - (dolist (buffer (eglot--managed-buffers proc)) + (dolist (buffer (eglot--managed-buffers server)) (with-current-buffer buffer (funcall (or eglot--current-flymake-report-fn #'ignore) eglot--unreported-diagnostics))))) From 40d7fc6f043ee45549c1a7334d853f9a2c5ad231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 22 May 2018 11:14:08 +0100 Subject: [PATCH 173/771] Do rust's rls hack properly with new class-based api * eglot.el (eglot-server-ready-p): New API method. (eglot-handle-request, eglot-handle-notification): New defgeneric's. (eglot--ready-predicates, eglot--server-ready-p): Remove. (eglot--async-request): Call eglot-server-ready-p. (eglot--request): Tweak comment. (eglot--rls-probably-ready-for-p): Remove. (eglot-server-ready-p eglot-rls): Adapts earlier eglot--rls-probably-ready-for-p. (eglot-handle-notification eglot-rls): Specialize to eglot-rls. (eglot-rls): New eglot-lsp-server subclass. * eglot-tests.el (auto-detect-running-server) (auto-reconnect, basic-completions) (hover-after-completions): Use eglot--interactive --- lisp/progmodes/eglot.el | 61 +++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cb213e515c4..43cda1b42ef 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -68,7 +68,7 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-server-programs '((rust-mode . ("rls")) +(defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) (python-mode . ("pyls")) (js-mode . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) @@ -112,6 +112,23 @@ lasted more than that many seconds." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) + +;;; API +;;; +(cl-defgeneric eglot-server-ready-p (server what) ;; API + "Tell if SERVER is ready for WHAT in current buffer. +If it isn't, a deferrable `eglot--async-request' *will* be +deferred to the future." + (:method (_s _what) + "Normally not ready if outstanding changes." + (not (eglot--outstanding-edits-p)))) + +(cl-defgeneric eglot-handle-request (server method id &rest params) + "Handle SERVER's METHOD request with ID and PARAMS.") + +(cl-defgeneric eglot-handle-notification (server method id &rest params) + "Handle SERVER's METHOD notification with PARAMS.") + ;;; Process management (defvar eglot--servers-by-project (make-hash-table :test #'equal) @@ -619,16 +636,6 @@ originated." (eglot--log-event server `(:running-deferred ,(length actions))) (mapc #'funcall (mapcar #'car actions)))) -(defvar eglot--ready-predicates '(eglot--server-ready-p) - "Special hook of predicates controlling deferred actions. -If one of these returns nil, a deferrable `eglot--async-request' -will be deferred. Each predicate is passed the symbol for the -request request and a process object.") - -(defun eglot--server-ready-p (_what _server) - "Tell if SERVER is ready for processing deferred WHAT." - (not (eglot--outstanding-edits-p))) - (cl-defmacro eglot--lambda (cl-lambda-list &body body) (declare (indent 1) (debug (sexp &rest form))) (let ((e (gensym "eglot--lambda-elem"))) @@ -668,8 +675,7 @@ TIMER)." (existing (gethash (list deferred buf) (eglot--deferred-actions server)))) (when existing (setq existing (cadr existing))) - (if (run-hook-with-args-until-failure 'eglot--ready-predicates - deferred server) + (if (eglot-server-ready-p server deferred) (remhash (list deferred buf) (eglot--deferred-actions server)) (eglot--log-event server `(:deferring ,method :id ,id :params ,params)) (let* ((buf (current-buffer)) (point (point)) @@ -708,9 +714,9 @@ TIMER)." "Like `eglot--async-request' for SERVER, METHOD and PARAMS, but synchronous. Meaning only return locally if successful, otherwise exit non-locally. DEFERRED is passed to `eglot--async-request', which see." - ;; Launching a deferred sync request with outstanding changes is a - ;; bad idea, since that might lead to the request never having a - ;; chance to run, because `eglot--ready-predicates'. + ;; HACK: A deferred sync request with outstanding changes is a bad + ;; idea, since that might lead to the request never having a chance + ;; to run, because idle timers don't run in `accept-process-output'. (when deferred (eglot--signal-textDocument/didChange)) (let* ((done (make-symbol "eglot-catch")) id-and-timer (res @@ -1583,22 +1589,19 @@ Proceed? " ;;; Rust-specific ;;; -(defun eglot--rls-probably-ready-for-p (what server) - "Guess if the RLS running in SERVER is ready for WHAT." - (or (eq what :textDocument/completion) ; RLS normally ready for this - ; one, even if building ; - (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner server))) - (and (equal "Indexing" what) done)))) +(defclass eglot-rls (eglot-lsp-server) () :documentation "Rustlang's RLS.") -;;;###autoload -(progn - (add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) - (defun eglot--setup-rls-idiosyncrasies () - "Prepare `eglot' to deal with RLS's special treatment." - (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t))) +(cl-defmethod eglot-server-ready-p ((server eglot-rls) what) + "Except for :completion, RLS isn't ready until Indexing done." + (and (cl-call-next-method) + (or ;; RLS normally ready for this, even if building. + (eq :textDocument/completion what) + (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner server))) + (and (equal "Indexing" what) done))))) (cl-defmethod eglot-handle-notification - (server (_method (eql :window/progress)) &key id done title message &allow-other-keys) + ((server eglot-rls) (_method (eql :window/progress)) + &key id done title message &allow-other-keys) "Handle notification window/progress" (setf (eglot--spinner server) (list id title done message)) (when (and (equal "Indexing" title) done) From c765121f67df7f7cdc03e16b1130f4e508b39a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 22 May 2018 11:41:02 +0100 Subject: [PATCH 174/771] Introduce new api methods for experimental clients to use Should help Josh Elsasser implement pull request https://github.com/joaotavora/eglot/issues/6. * eglot.el (eglot--obj): Move upwards in file. (eglot-server-ready-p): Tweak comment. (eglot-initialization-options): New API defgeneric.. (eglot-client-capabilities): New API defgeneric. (eglot--client-capabilities): Remove. (eglot--connect): Call new API methods here. --- lisp/progmodes/eglot.el | 85 +++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 43cda1b42ef..5e9bdc3566b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -115,12 +115,19 @@ lasted more than that many seconds." ;;; API ;;; +(defmacro eglot--obj (&rest what) + "Make WHAT a JSON object suitable for `json-encode'." + (declare (debug (&rest form))) + ;; FIXME: not really API. Should it be? + ;; FIXME: maybe later actually do something, for now this just fixes + ;; the indenting of literal plists. + `(list ,@what)) + (cl-defgeneric eglot-server-ready-p (server what) ;; API "Tell if SERVER is ready for WHAT in current buffer. If it isn't, a deferrable `eglot--async-request' *will* be deferred to the future." - (:method (_s _what) - "Normally not ready if outstanding changes." + (:method (_s _what) "Normally ready if no outstanding changes." (not (eglot--outstanding-edits-p)))) (cl-defgeneric eglot-handle-request (server method id &rest params) @@ -129,6 +136,35 @@ deferred to the future." (cl-defgeneric eglot-handle-notification (server method id &rest params) "Handle SERVER's METHOD notification with PARAMS.") +(cl-defgeneric eglot-initialization-options (server) + "JSON object to send under `initializationOptions'" + (:method (_s) nil)) ; blank default + +(cl-defgeneric eglot-client-capabilities (server) + "What the EGLOT LSP client supports for SERVER." + (:method (_s) + (eglot--obj + :workspace (eglot--obj + :applyEdit t + :workspaceEdit `(:documentChanges :json-false) + :didChangeWatchesFiles `(:dynamicRegistration t) + :symbol `(:dynamicRegistration :json-false)) + :textDocument + (eglot--obj + :synchronization (eglot--obj + :dynamicRegistration :json-false + :willSave t :willSaveWaitUntil t :didSave t) + :completion `(:dynamicRegistration :json-false) + :hover `(:dynamicRegistration :json-false) + :signatureHelp `(:dynamicRegistration :json-false) + :references `(:dynamicRegistration :json-false) + :definition `(:dynamicRegistration :json-false) + :documentSymbol `(:dynamicRegistration :json-false) + :documentHighlight `(:dynamicRegistration :json-false) + :rename `(:dynamicRegistration :json-false) + :publishDiagnostics `(:relatedInformation :json-false)) + :experimental (eglot--obj)))) + ;;; Process management (defvar eglot--servers-by-project (make-hash-table :test #'equal) @@ -223,13 +259,6 @@ CONTACT is in `eglot'. Returns a process object." (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t))) proc)) -(defmacro eglot--obj (&rest what) - "Make WHAT a suitable argument for `json-encode'." - (declare (debug (&rest form))) - ;; FIXME: maybe later actually do something, for now this just fixes - ;; the indenting of literal plists. - `(list ,@what)) - (defun eglot--all-major-modes () "Return all know major modes." (let ((retval)) @@ -238,29 +267,6 @@ CONTACT is in `eglot'. Returns a process object." (push sym retval)))) retval)) -(defun eglot--client-capabilities () - "What the EGLOT LSP client supports." - (eglot--obj - :workspace (eglot--obj - :applyEdit t - :workspaceEdit `(:documentChanges :json-false) - :didChangeWatchesFiles `(:dynamicRegistration t) - :symbol `(:dynamicRegistration :json-false)) - :textDocument (eglot--obj - :synchronization (eglot--obj - :dynamicRegistration :json-false - :willSave t :willSaveWaitUntil t :didSave t) - :completion `(:dynamicRegistration :json-false) - :hover `(:dynamicRegistration :json-false) - :signatureHelp `(:dynamicRegistration :json-false) - :references `(:dynamicRegistration :json-false) - :definition `(:dynamicRegistration :json-false) - :documentSymbol `(:dynamicRegistration :json-false) - :documentHighlight `(:dynamicRegistration :json-false) - :rename `(:dynamicRegistration :json-false) - :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (eglot--obj))) - (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") (defun eglot--connect (project managed-major-mode contact server-class) @@ -294,15 +300,12 @@ class SERVER-CLASS." (eglot--request server :initialize - (eglot--obj :processId (unless (eq (process-type proc) - 'network) - (emacs-pid)) - :capabilities(eglot--client-capabilities) - :rootPath (expand-file-name - (car (project-roots project))) - :rootUri (eglot--path-to-uri - (car (project-roots project))) - :initializationOptions [])) + (eglot--obj + :processId (unless (eq (process-type proc) 'network) (emacs-pid)) + :capabilities (eglot-client-capabilities) + :rootPath (expand-file-name (car (project-roots project))) + :rootUri (eglot--path-to-uri (car (project-roots project))) + :initializationOptions (eglot-initialization-options server))) (setf (eglot--capabilities server) capabilities) (setf (eglot--status server) nil) (dolist (buffer (buffer-list)) From 0ebd4a816dff264519407694ed7d8d39bbce9c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 22 May 2018 11:42:12 +0100 Subject: [PATCH 175/771] Fix indentation broken by the defclass monster commit * eglot.el (for, eglot-handle-notification publishDiagnostics) (eglot-handle-request registerCapability, eglot-handle-request unregisterCapability, eglot-handle-request applyEdit): fix indentation. --- lisp/progmodes/eglot.el | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5e9bdc3566b..3d363444405 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -113,7 +113,7 @@ lasted more than that many seconds." (integer :tag "Number of seconds"))) -;;; API +;;; API (WORK-IN-PROGRESS!) ;;; (defmacro eglot--obj (&rest what) "Make WHAT a JSON object suitable for `json-encode'." @@ -302,7 +302,7 @@ class SERVER-CLASS." :initialize (eglot--obj :processId (unless (eq (process-type proc) 'network) (emacs-pid)) - :capabilities (eglot-client-capabilities) + :capabilities (eglot-client-capabilities server) :rootPath (expand-file-name (car (project-roots project))) :rootUri (eglot--path-to-uri (car (project-roots project))) :initializationOptions (eglot-initialization-options server))) @@ -820,15 +820,15 @@ If optional MARKER, return a marker instead" (insert string) (font-lock-ensure) (buffer-string)))) (defun eglot--server-capable (&rest feats) -"Determine if current server is capable of FEATS." -(cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) - then (cadr probe) - for feat in feats - for probe = (plist-member caps feat) - if (not probe) do (cl-return nil) - if (eq (cadr probe) t) do (cl-return t) - if (eq (cadr probe) :json-false) do (cl-return nil) - finally (cl-return (or probe t)))) + "Determine if current server is capable of FEATS." + (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) + then (cadr probe) + for feat in feats + for probe = (plist-member caps feat) + if (not probe) do (cl-return nil) + if (eq (cadr probe) t) do (cl-return t) + if (eq (cadr probe) :json-false) do (cl-return nil) + finally (cl-return (or probe t)))) (defun eglot--range-region (range &optional markers) "Return region (BEG END) that represents LSP RANGE. @@ -1032,7 +1032,7 @@ function with the server still running." "Unreported diagnostics for this buffer.") (cl-defmethod eglot-handle-notification - (_server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) + (_server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1073,18 +1073,18 @@ THINGS are either registrations or unregisterations." (eglot--reply server jsonrpc-id :result (eglot--obj :message "OK"))) (cl-defmethod eglot-handle-request - (server id (_method (eql :client/registerCapability)) &key registrations) + (server id (_method (eql :client/registerCapability)) &key registrations) "Handle server request client/registerCapability" (eglot--register-unregister server id registrations 'register)) (cl-defmethod eglot-handle-request (server id (_method (eql :client/unregisterCapability)) - &key unregisterations) ;; XXX: "unregisterations" (sic) + &key unregisterations) ;; XXX: "unregisterations" (sic) "Handle server request client/unregisterCapability" (eglot--register-unregister server id unregisterations 'unregister)) (cl-defmethod eglot-handle-request - (server id (_method (eql :workspace/applyEdit)) &key _label edit) + (server id (_method (eql :workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) From e4039bba6d27d0746175a7f787364d1a3dcf2f87 Mon Sep 17 00:00:00 2001 From: Josh Elsasser Date: Mon, 21 May 2018 12:27:05 -0700 Subject: [PATCH 176/771] Add cquery support for c/c++ projects Implements minimal support for the core cquery language server. None of its extensions are implemented yet. * eglot.el (eglot-server-programs): Add cquery to list of guessed programs for c-mode and c++-mode. (eglot-initialization-options eglot-cquery): Specialize init options to pass cquery a cache directory and disable a flood of $cquery/progress messages. (eglot-handle-notification $cquery/publishSemanticHighlighting): (eglot-handle-notification $cquery/setInactiveRegions): (eglot-handle-notification $cquery/progress): New no-op functions to avoid filling logs with "unknown message" warnings. (eglot-cquery): New eglot-lsp-server subclass. * README.md: Mention cquery in the README. --- lisp/progmodes/eglot.el | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d363444405..189ad1e5aae 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -72,6 +72,8 @@ (python-mode . ("pyls")) (js-mode . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) + (c++-mode . (eglot-cquery "cquery")) + (c-mode . (eglot-cquery "cquery")) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) "How the command `eglot' guesses the server to start. @@ -1613,6 +1615,34 @@ Proceed? " (funcall (or eglot--current-flymake-report-fn #'ignore) eglot--unreported-diagnostics))))) + +;;; cquery-specific +;;; +(defclass eglot-cquery (eglot-lsp-server) () + :documentation "cquery's C/C++ langserver.") + +(cl-defmethod eglot-initialization-options ((server eglot-cquery)) + "Passes through required cquery initialization options" + (let* ((root (car (project-roots (eglot--project server)))) + (cache (expand-file-name ".cquery_cached_index/" root))) + (vector :cacheDirectory (file-name-as-directory cache) + :progressReportFrequencyMs -1))) + +(cl-defmethod eglot-handle-notification + ((server eglot-cquery) (_method (eql :$cquery/progress)) + &rest counts &key activeThreads &allow-other-keys) + "No-op for noisy $cquery/progress extension") + +(cl-defmethod eglot-handle-notification + ((server eglot-cquery) (_method (eql :$cquery/setInactiveRegions)) + &key uri inactiveRegions &allow-other-keys) + "No-op for unsupported $cquery/setInactiveRegions extension") + +(cl-defmethod eglot-handle-notification + ((server eglot-cquery) (_method (eql :$cquery/publishSemanticHighlighting)) + &key uri symbols &allow-other-keys) + "No-op for unsupported $cquery/publishSemanticHighlighting extension") + (provide 'eglot) ;;; eglot.el ends here From 3650efa65ee1e34c36464c425e8cbb06cb317a8b Mon Sep 17 00:00:00 2001 From: Josh Elsasser Date: Mon, 21 May 2018 17:03:55 -0700 Subject: [PATCH 177/771] Introduce eglot--debug for unimportant messages * eglot.el (eglot--debug): New function to log noisy or trivial messages to the eglot events buffer. --- lisp/progmodes/eglot.el | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 189ad1e5aae..92f16c0d04f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -774,6 +774,10 @@ DEFERRED is passed to `eglot--async-request', which see." (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) +(defun eglot--debug (server format &rest args) + "Warning message with FORMAT and ARGS." + (eglot--log-event server `(:message ,(format format args)))) + (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion From 45d4814a806ee611ae9ef2b2acc8927cf8903b90 Mon Sep 17 00:00:00 2001 From: Josh Elsasser Date: Mon, 21 May 2018 17:15:12 -0700 Subject: [PATCH 178/771] Demote unvisited diagnostics logging to debug level The PublishDiagnostic spec (LSP Specification, 3.0) does not strictly forbid the server from publishing diagnostics before a file has been visited. * eglot.el (eglot--server-textDocument/publishDiagnostics): Log the "received diagnostics for unvisited file" warning as debug to avoid spamming users of compliant language servers. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 92f16c0d04f..467e524b073 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1038,7 +1038,7 @@ function with the server still running." "Unreported diagnostics for this buffer.") (cl-defmethod eglot-handle-notification - (_server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) + (server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -1060,7 +1060,7 @@ function with the server still running." (setq eglot--unreported-diagnostics nil)) (t (setq eglot--unreported-diagnostics diags))))) - (eglot--warn "Diagnostics received for unvisited %s" uri))) + (eglot--debug server "Diagnostics received for unvisited %s" uri))) (cl-defun eglot--register-unregister (server jsonrpc-id things how) "Helper for `registerCapability'. From 9c14cfd179b43f1de87bbb0aca83ba0c34288eb4 Mon Sep 17 00:00:00 2001 From: Josh Elsasser Date: Thu, 24 May 2018 20:10:15 -0700 Subject: [PATCH 179/771] Log debug messages through eglot--debug * eglot.el (eglot--async-request, eglot--process-sentinel): (eglot--call-deferred): Use eglot--debug to log messages to the server events buffer. (eglot--server-receive): Demote "Notification unimplemented" message on missing handlers to a pure debug message. --- lisp/progmodes/eglot.el | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 467e524b073..55fa40649c9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -432,7 +432,7 @@ INTERACTIVE is t if called interactively." (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." (let ((server (process-get proc 'eglot-server))) - (eglot--log-event server `(:message "Process state changed" :change ,change)) + (eglot--debug server "Process state changed: %s" change) (when (not (process-live-p proc)) (with-current-buffer (eglot-events-buffer server) (let ((inhibit-read-only t)) @@ -597,8 +597,7 @@ originated." (condition-case-unless-debug _err (apply #'eglot-handle-notification server method params) (cl-no-applicable-method - (eglot--log-event - server '(:error `(:message "Notification unimplemented")))))) + (eglot--debug server "Notification unimplemented: %s" method)))) (continuations (cancel-timer (cl-third continuations)) (remhash id (eglot--pending-continuations server)) @@ -638,7 +637,7 @@ originated." (defun eglot--call-deferred (server) "Call SERVER's deferred actions, who may again defer themselves." (when-let ((actions (hash-table-values (eglot--deferred-actions server)))) - (eglot--log-event server `(:running-deferred ,(length actions))) + (eglot--debug server "running %d deferred actions" (length actions)) (mapc #'funcall (mapcar #'car actions)))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) @@ -682,7 +681,7 @@ TIMER)." (when existing (setq existing (cadr existing))) (if (eglot-server-ready-p server deferred) (remhash (list deferred buf) (eglot--deferred-actions server)) - (eglot--log-event server `(:deferring ,method :id ,id :params ,params)) + (eglot--debug server "deferring %s (id %s)" method id) (let* ((buf (current-buffer)) (point (point)) (later (lambda () (when (buffer-live-p buf) @@ -704,8 +703,7 @@ TIMER)." (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) - (eglot--log-event - server (eglot--obj :message "success ignored" :id id)))) + (eglot--debug server "%s success ignored (id %s)" method id))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) (setf (eglot--status server) `(,message t)) From 8a7d17b5ea378579a251dbd8409b46f158a86a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 26 May 2018 01:07:10 +0100 Subject: [PATCH 180/771] Document current api breaches a bit * eglot-tests.el (eglot--call-with-dirs-and-files) (auto-reconnect): use eglot--process * eglot.el (eglot-shutdown, eglot, eglot-reconnect) (eglot--connect): Use eglot--process (eglot--process): Alias to concentrate the hack here. (eglot--signal-textDocument/didChange): Tweak comment. --- lisp/progmodes/eglot.el | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 132833112d5..ccb7b4908ae 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -191,6 +191,11 @@ lasted more than that many seconds." (defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") +;; HACK: Do something to fix this in the jsonrpc API or here, but in +;; the meantime concentrate the hack here. +(defalias 'eglot--process 'jsonrpc--process + "An abuse of `jsonrpc--process', a jsonrpc.el internal.") + (defun eglot-shutdown (server &optional _interactive) "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond. Don't leave this @@ -206,9 +211,9 @@ function with the server still running." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) - (when (process-live-p (jsonrpc--process server)) + (when (process-live-p (eglot--process server)) (eglot--warn "Brutally deleting non-compliant server %s" (jsonrpc-name server)) - (delete-process (jsonrpc--process server))))) + (delete-process (eglot--process server))))) (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." @@ -330,7 +335,7 @@ INTERACTIVE is t if called interactively." (car (project-roots project))))) (current-server (jsonrpc-current-connection)) (live-p (and current-server - (process-live-p (jsonrpc--process current-server))))) + (process-live-p (eglot--process current-server))))) (if (and live-p interactive (y-or-n-p "[eglot] Live process found, reconnect instead? ")) @@ -351,7 +356,7 @@ managing `%s' buffers in project `%s'." "Reconnect to SERVER. INTERACTIVE is t if called interactively." (interactive (list (jsonrpc-current-connection-or-lose) t)) - (when (process-live-p (jsonrpc--process server)) + (when (process-live-p (eglot--process server)) (eglot-shutdown server interactive)) (eglot--connect (eglot--project server) (eglot--major-mode server) @@ -391,7 +396,7 @@ And NICKNAME and CONTACT." server :initialize (jsonrpc-obj :processId (unless (eq (process-type - (jsonrpc--process server)) + (eglot--process server)) 'network) (emacs-pid)) :rootPath (expand-file-name @@ -415,7 +420,7 @@ And NICKNAME and CONTACT." (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) (setq success server)) - (unless (or success (not (process-live-p (jsonrpc--process server))) + (unless (or success (not (process-live-p (eglot--process server))) (eglot--moribund server)) (eglot-shutdown server))))) @@ -851,7 +856,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) (setf (eglot--spinner server) (list nil :textDocument/didChange t)) - ;; HACK! + ;; HACK! perhaps jsonrpc should just call this on every send (jsonrpc--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () From 1dcdc11127a21f1018d4a87b23295c1a113e4350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 26 May 2018 16:11:11 +0100 Subject: [PATCH 181/771] Don't rely on flymake's idle timer for textdocument/didchange * eglot.el (eglot--after-change): Set idle timer here. (eglot--change-idle-timer): New var. (eglot--signal-textDocument/didChange): No seed to set spinner here. (eglot-flymake-backend) Don't send didChange here. --- lisp/progmodes/eglot.el | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d363444405..8fe24ed1978 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1131,6 +1131,8 @@ THINGS are either registrations or unregisterations." (cl-plusp (+ (length (car eglot--recent-changes)) (length (cdr eglot--recent-changes))))) +(defvar eglot--change-idle-timer nil "Idle timer for textDocument/didChange.") + (defun eglot--before-change (start end) "Hook onto `before-change-functions'. Records START and END, crucially convert them into @@ -1149,7 +1151,12 @@ Records START, END and PRE-CHANGE-LENGTH locally." (setf (cdr eglot--recent-changes) (vconcat (cdr eglot--recent-changes) `[(,pre-change-length - ,(buffer-substring-no-properties start end))]))) + ,(buffer-substring-no-properties start end))])) + (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) + (setq eglot--change-idle-timer + (run-with-idle-timer + 0.5 nil (lambda () (eglot--signal-textDocument/didChange) + (setq eglot--change-idle-timer nil))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." @@ -1166,8 +1173,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--notify server :textDocument/didChange (eglot--obj - :textDocument - (eglot--VersionedTextDocumentIdentifier) + :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector (eglot--obj @@ -1180,7 +1186,6 @@ Records START, END and PRE-CHANGE-LENGTH locally." :rangeLength len :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) - (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () @@ -1225,9 +1230,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." ;; Report anything unreported (when eglot--unreported-diagnostics (funcall report-fn eglot--unreported-diagnostics) - (setq eglot--unreported-diagnostics nil)) - ;; Signal a didChange that might eventually bring new diagnotics - (eglot--signal-textDocument/didChange)) + (setq eglot--unreported-diagnostics nil))) (defun eglot-xref-backend () "EGLOT xref backend." From 545e9c8a1314aa095f6e12e9764011441f182bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 26 May 2018 16:13:53 +0100 Subject: [PATCH 182/771] Simpify eglot--server-receive * eglot.el (eglot--obj): Cleanup whitespace. (eglot--server-receive): Simplify. --- lisp/progmodes/eglot.el | 45 ++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8fe24ed1978..51bcc8bd2d2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -115,7 +115,7 @@ lasted more than that many seconds." ;;; API (WORK-IN-PROGRESS!) ;;; -(defmacro eglot--obj (&rest what) +(defmacro eglot--obj (&rest what) "Make WHAT a JSON object suitable for `json-encode'." (declare (debug (&rest form))) ;; FIXME: not really API. Should it be? @@ -582,29 +582,28 @@ originated." (gethash id (eglot--pending-continuations server))))) (eglot--log-event server message 'server) (when error (setf (eglot--status server) `(,error t))) - (unless (or (null method) - (keywordp method)) + (unless (or (null method) (keywordp method)) (setq method (intern (format ":%s" method)))) - (cond ((and method id) - (condition-case-unless-debug _err - (apply #'eglot-handle-request server id method params) - (cl-no-applicable-method - (eglot--reply server id - :error `(:code -32601 :message "Method unimplemented"))))) - (method - (condition-case-unless-debug _err - (apply #'eglot-handle-notification server method params) - (cl-no-applicable-method - (eglot--log-event - server '(:error `(:message "Notification unimplemented")))))) - (continuations - (cancel-timer (cl-third continuations)) - (remhash id (eglot--pending-continuations server)) - (if error - (funcall (cl-second continuations) error) - (funcall (cl-first continuations) result))) - (id - (eglot--warn "Ooops no continuation for id %s" id))) + (cond + (method + (condition-case-unless-debug _err + (if id + (apply #'eglot-handle-request server id method params) + (apply #'eglot-handle-notification server method params)) + (cl-no-applicable-method + (if id + (eglot--reply + server id :error `(:code -32601 :message "Method unimplemented")) + (eglot--log-event + server '(:error `(:message "Notification unimplemented"))))))) + (continuations + (cancel-timer (cl-third continuations)) + (remhash id (eglot--pending-continuations server)) + (if error + (funcall (cl-second continuations) error) + (funcall (cl-first continuations) result))) + (id + (eglot--warn "Ooops no continuation for id %s" id))) (eglot--call-deferred server) (force-mode-line-update t)))) From f165670762c427d49446ad9002d8f84c176cd9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 26 May 2018 16:22:46 +0100 Subject: [PATCH 183/771] Cleanup deferred request mechanism with a readable log * eglot.el (eglot-lsp-server): Rework doc of deferred-actions slot. (defvar eglot--next-request-id): Move down, now buffer local. (defun eglot--next-request-id): Remove. (eglot--call-deferred): Be more informative. (eglot--async-request): Simplify. --- lisp/progmodes/eglot.el | 111 ++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 61 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 51bcc8bd2d2..1a4e3c26773 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -211,8 +211,8 @@ deferred to the future." :documentation "How server was started and how it can be re-started." :initarg :contact :accessor eglot--contact) (deferred-actions - :documentation "Map (DEFERRED-ID BUF) to (FN TIMER). -DEFERRED request from BUF is FN. It's sent later, not later than TIMER." + :documentation "Map (DEFERRED BUF) to (FN TIMER ID). FN is a saved\ +DEFERRED request from BUF, to be sent not later than TIMER as ID." :initform (make-hash-table :test #'equal) :accessor eglot--deferred-actions) (file-watches :documentation "Map ID to list of WATCHES for `didChangeWatchedFiles'." @@ -615,12 +615,6 @@ originated." (string-bytes json) json)) (eglot--log-event server message 'client))) -(defvar eglot--next-request-id 0 "ID for next request.") - -(defun eglot--next-request-id () - "Compute the next id for a client request." - (setq eglot--next-request-id (1+ eglot--next-request-id))) - (defun eglot-forget-pending-continuations (server) "Stop waiting for responses from the current LSP SERVER." (interactive (list (eglot--current-server-or-lose))) @@ -635,7 +629,7 @@ originated." (defun eglot--call-deferred (server) "Call SERVER's deferred actions, who may again defer themselves." (when-let ((actions (hash-table-values (eglot--deferred-actions server)))) - (eglot--log-event server `(:running-deferred ,(length actions))) + (eglot--log-event server `(:maybe-run-deferred ,(mapcar #'caddr actions))) (mapc #'funcall (mapcar #'car actions)))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) @@ -643,6 +637,8 @@ originated." (let ((e (gensym "eglot--lambda-elem"))) `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) +(defvar-local eglot--next-request-id 0 "ID for next `eglot--async-request'.") + (cl-defun eglot--async-request (server method params @@ -656,59 +652,52 @@ objects, respectively. Wait TIMEOUT seconds for response or call nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the future, or to never at all, in case a new request with identical DEFERRED and for the same buffer overrides it (however, if that -happens, the original timeout keeps counting). Return a list (ID -TIMER)." - (let* ((id (eglot--next-request-id)) - (timer nil) - (make-timer - (lambda ( ) - (or timer - (run-with-timer - timeout nil - (lambda () - (remhash id (eglot--pending-continuations server)) - (funcall (or timeout-fn - (lambda () - (eglot--log-event - server `(:timed-out ,method :id ,id - :params ,params))))))))))) +happens, the original timer keeps counting). Return (ID TIMER)." + (pcase-let* ( (buf (current-buffer)) (pos (point-marker)) + (`(,_ ,timer ,old-id) + (and deferred (gethash (list deferred buf) + (eglot--deferred-actions server)))) + (id (or old-id (cl-incf eglot--next-request-id))) + (make-timer + (lambda ( ) + (run-with-timer + timeout nil + (lambda () + (remhash id (eglot--pending-continuations server)) + (if timeout-fn (funcall timeout-fn) + (eglot--log-event + server `(:timed-out ,method :id ,id :params ,params)))))))) (when deferred - (let* ((buf (current-buffer)) - (existing (gethash (list deferred buf) - (eglot--deferred-actions server)))) - (when existing (setq existing (cadr existing))) - (if (eglot-server-ready-p server deferred) - (remhash (list deferred buf) (eglot--deferred-actions server)) - (eglot--log-event server `(:deferring ,method :id ,id :params ,params)) - (let* ((buf (current-buffer)) (point (point)) - (later (lambda () - (when (buffer-live-p buf) - (with-current-buffer buf - (save-excursion - (goto-char point) - (apply #'eglot--async-request server - method params args))))))) - (puthash (list deferred buf) - (list later (setq timer (funcall make-timer))) - (eglot--deferred-actions server)) - (cl-return-from eglot--async-request nil))))) - ;; Really run it - ;; - (eglot--send server (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)) - (puthash id - (list (or success-fn - (eglot--lambda (&rest _ignored) - (eglot--log-event - server (eglot--obj :message "success ignored" :id id)))) - (or error-fn - (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status server) `(,message t)) - server (eglot--obj :message "error ignored, status set" - :id id :error code))) - (setq timer (funcall make-timer))) + (if (eglot-server-ready-p server deferred) + ;; Server is ready, we jump below and send it immediately. + (remhash (list deferred buf) (eglot--deferred-actions server)) + ;; Otherwise, save in `eglot--deferred-actions' and exit non-locally + (unless old-id + ;; Also, if it's the first deferring for this id, inform the log + (eglot--log-event server `(:deferring ,method :id ,id :params ,params))) + (puthash (list deferred buf) + (list (lambda () (when (buffer-live-p buf) + (with-current-buffer buf + (save-excursion + (goto-char pos) + (apply #'eglot--async-request server + method params args))))) + (or timer (funcall make-timer)) id) + (eglot--deferred-actions server)) + (cl-return-from eglot--async-request nil))) + ;; Really send the request + (eglot--send server `(:jsonrpc "2.0" :id ,id :method ,method :params ,params)) + (puthash id (list + (or success-fn + (eglot--lambda (&rest _ignored) + (eglot--log-event + server (eglot--obj :message "success ignored" :id id)))) + (or error-fn + (eglot--lambda (&key code message &allow-other-keys) + (setf (eglot--status server) `(,message t)) + server (eglot--obj :message "error ignored, status set" + :id id :error code))) + (or timer (funcall make-timer))) (eglot--pending-continuations server)) (list id timer))) From 94998753703afa546a96e460d96a4c58655e9927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 26 May 2018 16:37:10 +0100 Subject: [PATCH 184/771] Get rid of eglot--obj, an uninteresting abstraction * eglot.el (eglot--obj): Get rid of this. It wasn't widely used anyway. (eglot-client-capabilities) (eglot--connect, eglot--async-request, eglot--notify) (eglot--reply, eglot--pos-to-lsp-position, eglot-handle-request) (eglot--register-unregister, eglot-handle-request) (eglot--TextDocumentIdentifier) (eglot--VersionedTextDocumentIdentifier) (eglot--TextDocumentItem, eglot--TextDocumentPositionParams) (eglot--signal-textDocument/didChange) (eglot--signal-textDocument/didSave) (xref-backend-identifier-completion-table) (xref-backend-references, xref-backend-apropos, eglot-imenu) (eglot-rename): Use list instead of eglot--obj. --- lisp/progmodes/eglot.el | 115 +++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 66 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1a4e3c26773..b83e772c49e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -115,14 +115,6 @@ lasted more than that many seconds." ;;; API (WORK-IN-PROGRESS!) ;;; -(defmacro eglot--obj (&rest what) - "Make WHAT a JSON object suitable for `json-encode'." - (declare (debug (&rest form))) - ;; FIXME: not really API. Should it be? - ;; FIXME: maybe later actually do something, for now this just fixes - ;; the indenting of literal plists. - `(list ,@what)) - (cl-defgeneric eglot-server-ready-p (server what) ;; API "Tell if SERVER is ready for WHAT in current buffer. If it isn't, a deferrable `eglot--async-request' *will* be @@ -143,15 +135,15 @@ deferred to the future." (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." (:method (_s) - (eglot--obj - :workspace (eglot--obj + (list + :workspace (list :applyEdit t :workspaceEdit `(:documentChanges :json-false) :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) :textDocument - (eglot--obj - :synchronization (eglot--obj + (list + :synchronization (list :dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t) :completion `(:dynamicRegistration :json-false) @@ -163,7 +155,7 @@ deferred to the future." :documentHighlight `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (eglot--obj)))) + :experimental (list)))) ;;; Process management @@ -300,7 +292,7 @@ class SERVER-CLASS." (eglot--request server :initialize - (eglot--obj + (list :processId (unless (eq (process-type proc) 'network) (emacs-pid)) :capabilities (eglot-client-capabilities server) :rootPath (expand-file-name (car (project-roots project))) @@ -311,7 +303,7 @@ class SERVER-CLASS." (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) - (eglot--notify server :initialized (eglot--obj :__dummy__ t)) + (eglot--notify server :initialized `(:__dummy__ t)) (run-hook-with-args 'eglot-connect-hook server) (setq connect-success server)) (unless (or connect-success @@ -691,12 +683,12 @@ happens, the original timer keeps counting). Return (ID TIMER)." (or success-fn (eglot--lambda (&rest _ignored) (eglot--log-event - server (eglot--obj :message "success ignored" :id id)))) + server `(:message "success ignored" :id ,id)))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) (setf (eglot--status server) `(,message t)) - server (eglot--obj :message "error ignored, status set" - :id id :error code))) + server `(:message "error ignored, status set" + :id ,id :error ,code))) (or timer (funcall make-timer))) (eglot--pending-continuations server)) (list id timer))) @@ -732,16 +724,14 @@ DEFERRED is passed to `eglot--async-request', which see." (cl-defun eglot--notify (server method params) "Notify SERVER of something, don't expect a reply.e" - (eglot--send server (eglot--obj :jsonrpc "2.0" - :method method - :params params))) + (eglot--send server `(:jsonrpc "2.0" :method ,method :params ,params))) (cl-defun eglot--reply (server id &key result error) "Reply to PROCESS's request ID with MESSAGE." (eglot--send - server`(:jsonrpc "2.0" :id ,id - ,@(when result `(:result ,result)) - ,@(when error `(:error ,error))))) + server `(:jsonrpc "2.0" :id ,id + ,@(when result `(:result ,result)) + ,@(when error `(:error ,error))))) ;;; Helpers @@ -763,9 +753,9 @@ DEFERRED is passed to `eglot--async-request', which see." (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (save-excursion - (eglot--obj :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE - :character (- (goto-char (or pos (point))) - (line-beginning-position))))) + (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE + :character (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. @@ -1003,10 +993,9 @@ function with the server still running." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (eglot--reply server id :result (eglot--obj :title reply)) + (eglot--reply server id :result `(:title ,reply)) (eglot--reply server id - :error (eglot--obj :code -32800 - :message "User cancelled")))))) + :error `(:code -32800 :message "User cancelled")))))) (cl-defmethod eglot-handle-notification (_server (_method (eql :window/logMessage)) &key _type _message) @@ -1058,7 +1047,7 @@ THINGS are either registrations or unregisterations." (eglot--reply server jsonrpc-id :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (eglot--reply server jsonrpc-id :result (eglot--obj :message "OK"))) + (eglot--reply server jsonrpc-id :result `(:message "OK"))) (cl-defmethod eglot-handle-request (server id (_method (eql :client/registerCapability)) &key registrations) @@ -1079,37 +1068,36 @@ THINGS are either registrations or unregisterations." (eglot--reply server id :result `(:applied ))) (error (eglot--reply server id :result `(:applied :json-false) - :error (eglot--obj :code -32001 - :message (format "%s" err)))))) + :error `(:code -32001 :message ,(format "%s" err)))))) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." - (eglot--obj :uri (eglot--path-to-uri buffer-file-name))) + (list :uri (eglot--path-to-uri buffer-file-name))) (defvar-local eglot--versioned-identifier 0) (defun eglot--VersionedTextDocumentIdentifier () "Compute VersionedTextDocumentIdentifier object for current buffer." (append (eglot--TextDocumentIdentifier) - (eglot--obj :version eglot--versioned-identifier))) + `(:version ,eglot--versioned-identifier))) (defun eglot--TextDocumentItem () "Compute TextDocumentItem object for current buffer." (append (eglot--VersionedTextDocumentIdentifier) - (eglot--obj :languageId - (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode)) - "unknown") - :text - (save-restriction - (widen) - (buffer-substring-no-properties (point-min) (point-max)))))) + (list :languageId + (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode)) + "unknown") + :text + (save-restriction + (widen) + (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." - (eglot--obj :textDocument (eglot--TextDocumentIdentifier) - :position (eglot--pos-to-lsp-position))) + (list :textDocument (eglot--TextDocumentIdentifier) + :position (eglot--pos-to-lsp-position))) (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") @@ -1160,19 +1148,18 @@ Records START, END and PRE-CHANGE-LENGTH locally." (widen) (eglot--notify server :textDocument/didChange - (eglot--obj + (list :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges (if full-sync-p (vector - (eglot--obj + (list :text (buffer-substring-no-properties (point-min) (point-max)))) (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) for (len after-text) across (cdr eglot--recent-changes) - vconcat `[,(eglot--obj :range (eglot--obj :start start-pos - :end end-pos) - :rangeLength len - :text after-text)]))))) + vconcat `[,(list :range `(:start ,start-pos :end ,end-pos) + :rangeLength len + :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) (eglot--call-deferred server)))) @@ -1206,7 +1193,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--notify (eglot--current-server-or-lose) :textDocument/didSave - (eglot--obj + (list ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. :text (buffer-substring-no-properties (point-min) (point-max)) :textDocument (eglot--TextDocumentIdentifier)))) @@ -1253,17 +1240,15 @@ DUMMY is ignored" (eglot--lambda (&key name kind location containerName) (propertize name :textDocumentPositionParams - (eglot--obj :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) + (list :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) :locations (list location) :kind kind :containerName containerName)) - (eglot--request server - :textDocument/documentSymbol - (eglot--obj - :textDocument text-id)))) + (eglot--request + server :textDocument/documentSymbol `(:textDocument ,text-id)))) (all-completions string eglot--xref-known-symbols)))))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) @@ -1301,8 +1286,7 @@ DUMMY is ignored" :textDocument/references (append params - (eglot--obj :context - (eglot--obj :includeDeclaration t))))))) + `(:context (:includeDeclaration t))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) @@ -1311,7 +1295,7 @@ DUMMY is ignored" (eglot--xref-make name uri (plist-get range :start)))) (eglot--request (eglot--current-server-or-lose) :workspace/symbol - (eglot--obj :query pattern))))) + (list :query pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." @@ -1466,8 +1450,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (plist-get (plist-get location :range) :start)))) (eglot--request (eglot--current-server-or-lose) :textDocument/documentSymbol - (eglot--obj - :textDocument (eglot--TextDocumentIdentifier)))))) + `(:textDocument ,(eglot--TextDocumentIdentifier)))))) (append (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries) @@ -1534,7 +1517,7 @@ Proceed? " (eglot--apply-workspace-edit (eglot--request (eglot--current-server-or-lose) :textDocument/rename `(,@(eglot--TextDocumentPositionParams) - ,@(eglot--obj :newName newname))) + :newName ,newname)) current-prefix-arg)) From 004702460d4516306d5a1bae640bce9a367715f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 26 May 2018 18:52:17 +0100 Subject: [PATCH 185/771] Really ensure eglot--shutdown deletes a process completely * eglot.el (eglot-lsp-server): rename slot "moribund" to "shutdown-requested" (eglot--connect): Don't check if shutdown was requested here. (eglot--process-sentinel): Set shutdown-requested to :sentinel-done here. (eglot-shutdown): use eglot--shutdown-requested. Improve check for process liveness. --- lisp/progmodes/eglot.el | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b83e772c49e..cc3fae1f54a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -184,9 +184,9 @@ deferred to the future." (capabilities :documentation "JSON object containing server capabilities." :accessor eglot--capabilities) - (moribund + (shutdown-requested :documentation "Flag set when server is shutting down." - :accessor eglot--moribund) + :accessor eglot--shutdown-requested) (project :documentation "Project associated with server." :initarg :project :accessor eglot--project) @@ -307,7 +307,7 @@ class SERVER-CLASS." (run-hook-with-args 'eglot-connect-hook server) (setq connect-success server)) (unless (or connect-success - (not (process-live-p proc)) (eglot--moribund server)) + (not (process-live-p proc))) (eglot-shutdown server))))) (defvar eglot--command-history nil @@ -454,7 +454,8 @@ INTERACTIVE is t if called interactively." (eglot--process server))) (delete-process proc) ;; Consider autoreconnecting - (cond ((eglot--moribund server)) + (cond ((eglot--shutdown-requested server) + (setf (eglot--shutdown-requested server) :sentinel-done)) ((not (eglot--inhibit-autoreconnect server)) (eglot--warn "Reconnecting after unexpected server exit") (eglot-reconnect server)) @@ -959,15 +960,17 @@ function with the server still running." (eglot--message "Asking %s politely to terminate" (eglot--name server)) (unwind-protect (let ((eglot-request-timeout 3)) - (setf (eglot--moribund server) t) + (setf (eglot--shutdown-requested server) t) (eglot--request server :shutdown nil) ;; this one is supposed to always fail, hence ignore-errors (ignore-errors (eglot--request server :exit nil))) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) - (when (process-live-p (eglot--process server)) - (eglot--warn "Brutally deleting non-compliant server %s" (eglot--name server)) + (while (progn (accept-process-output nil 0.1) + (not (eq (eglot--shutdown-requested server) :sentinel-done))) + (eglot--warn "Sentinel for %s still hasn't run, brutally deleting it!" + (eglot--process server)) (delete-process (eglot--process server))))) (cl-defmethod eglot-handle-notification From 9b468c1cc19a75455bdeddb10f0405e0612ebaa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 26 May 2018 19:29:51 +0100 Subject: [PATCH 186/771] * eglot.el (version): bump to 0.5 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8d34b1be050..095b296f24f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.4 +;; Version: 0.5 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From f20edc04ed67a086d608ed3af9702f09481e9c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 27 May 2018 11:41:24 +0100 Subject: [PATCH 187/771] Fix assorted cquery-related bugs Some versions of cquery send a :role key as part of the response to textDocument/documentHighlight. Ignore it for now. Also cquery sometimes send 0-length ranges upon which we now fallback to flymake-diag-region. Finally, in eglot-eldoc-funciton, the previous hack of calling the eglot--hover-info outside of the when-buffer-window macrolet contained a bug. It must be called in the correct buffer. Revert the hack and do it by querying from eglot.el if ert is running tests. * eglot.el (eglot--range-region): Return a cons and fallback to flymake-diag-region if server returned a useless range. (eglot-handle-notification, eglot--hover-info): Update call to eglot--range-region. (eglot-help-at-point): Ensure `eglot--hover-info` runs in right buffer. (eglot-eldoc-function): Don't abuse eldoc-last-message like this. Also update call to eglot--range-region. Consider ert-running-test (eglot--apply-text-edits): Use pcase-lambda. (ert): require it. --- lisp/progmodes/eglot.el | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 095b296f24f..b95757ccefd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -60,6 +60,7 @@ (require 'xref) (require 'subr-x) (require 'filenotify) +(require 'ert) ;;; User tweakable stuff @@ -817,10 +818,15 @@ If optional MARKER, return a marker instead" finally (cl-return (or probe t)))) (defun eglot--range-region (range &optional markers) - "Return region (BEG END) that represents LSP RANGE. + "Return region (BEG . END) that represents LSP RANGE. If optional MARKERS, make markers." - (list (eglot--lsp-position-to-point (plist-get range :start) markers) - (eglot--lsp-position-to-point (plist-get range :end) markers))) + (let* ((st (plist-get range :start)) + (beg (eglot--lsp-position-to-point st markers)) + (end (eglot--lsp-position-to-point (plist-get range :end) markers))) + ;; Fallback to `flymake-diag-region' if server botched the range + (if (/= beg end) (cons beg end) (flymake-diag-region + (current-buffer) (plist-get st :line) + (1- (plist-get st :character)))))) ;;; Minor modes @@ -1028,7 +1034,7 @@ function with the server still running." collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (pcase-let ((`(,beg ,end) (eglot--range-region range))) + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (flymake-make-diagnostic (current-buffer) beg end (cond ((<= severity 1) :error) @@ -1362,7 +1368,7 @@ DUMMY is ignored" (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") (defun eglot--hover-info (contents &optional range) - (concat (and range (pcase-let ((`(,beg ,end) (eglot--range-region range))) + (concat (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": "))) (mapconcat #'eglot--format-markup (append (cond ((vectorp contents) contents) @@ -1397,9 +1403,9 @@ DUMMY is ignored" (eglot--request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) - (with-help-window "*eglot help*" - (with-current-buffer standard-output - (insert (eglot--hover-info contents range)))))) + (let ((blurb (eglot--hover-info contents range))) + (with-help-window "*eglot help*" + (with-current-buffer standard-output (insert blurb)))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function. @@ -1409,8 +1415,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (position-params (eglot--TextDocumentPositionParams)) sig-showing) (cl-macrolet ((when-buffer-window - (&body body) `(when (get-buffer-window buffer) - (with-current-buffer buffer ,@body)))) + (&body body) + `(when (or (get-buffer-window buffer) (ert-running-test)) + (with-current-buffer buffer ,@body)))) (when (eglot--server-capable :signatureHelpProvider) (eglot--async-request server :textDocument/signatureHelp position-params @@ -1428,8 +1435,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." server :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) (unless sig-showing - (setq eldoc-last-message (eglot--hover-info contents range)) - (when-buffer-window (eldoc-message eldoc-last-message)))) + (when-buffer-window + (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request @@ -1438,8 +1445,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc #'delete-overlay eglot--highlights) (setq eglot--highlights (when-buffer-window - (mapcar (eglot--lambda (&key range _kind) - (pcase-let ((`(,beg ,end) + (mapcar (eglot--lambda (&key range _kind _role) + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) @@ -1475,7 +1482,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (save-restriction (widen) (save-excursion - (mapc (eglot--lambda (newText beg end) + (mapc (pcase-lambda (`(,newText ,beg . ,end)) (goto-char beg) (delete-region beg end) (insert newText)) (mapcar (eglot--lambda (&key range newText) (cons newText (eglot--range-region range 'markers))) From 92593bb4c13a80303866cdb6992adb2012e2b279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 27 May 2018 11:58:41 +0100 Subject: [PATCH 188/771] Set spinner in textdocument/didchange as it matters to rls Otherwise, the asynch eldoc action will immediately send the textDocument/documentHighlight requests, without understanding that they need to be deferred a bit more. * eglot.el (eglot--signal-textDocument/didChange): Set the spinner here. --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b95757ccefd..5fab5749c58 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1177,6 +1177,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." :rangeLength len :text after-text)]))))) (setq eglot--recent-changes (cons [] [])) + (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () From 22294e04cd996c3bd337611cbf32a462909a6273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 27 May 2018 12:58:49 +0100 Subject: [PATCH 189/771] Hopefully fix the flymake bootstrap problem Immediately after M-x eglot, eglot's use of flymake was having trouble detecting the first diagnostics sent from the server, resulting in an annoying "Wait" in the mode-line. * eglot.el (eglot--current-flymake-report-fn): Move up here. (eglot--managed-mode): Set eglot--current-flymake-report-fn to nil on teardown. (eglot--maybe-activate-editing-mode): Simplify. (eglot-handle-notification textDocument/publishDiagnostics): Set unreported-diagnostics to a cons. (eglot-handle-notification eglot-rls window/progress): Simplify. * eglot-tests.el (rls-basic-diagnostics): Simplify test. --- lisp/progmodes/eglot.el | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5fab5749c58..ce131958db4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -833,6 +833,9 @@ If optional MARKERS, make markers." ;;; (defvar eglot-mode-map (make-sparse-keymap)) +(defvar-local eglot--current-flymake-report-fn nil + "Current flymake report function for this buffer") + (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map @@ -862,7 +865,8 @@ If optional MARKERS, make markers." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (remove-function (local imenu-create-index-function) #'eglot-imenu)))) + (remove-function (local imenu-create-index-function) #'eglot-imenu) + (setq eglot--current-flymake-report-fn nil)))) (defun eglot--managed-mode-onoff (server arg) "Proxy for function `eglot--managed-mode' with ARG and SERVER." @@ -876,9 +880,6 @@ If optional MARKERS, make markers." (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) -(defvar-local eglot--current-flymake-report-fn nil - "Current flymake report function for this buffer") - (defun eglot--maybe-activate-editing-mode (&optional server) "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In @@ -888,9 +889,7 @@ that case, also signal textDocument/didOpen." (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server (eglot--managed-mode-onoff server 1) - (eglot--signal-textDocument/didOpen) - (flymake-start) - (funcall (or eglot--current-flymake-report-fn #'ignore) nil)))) + (eglot--signal-textDocument/didOpen)))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) @@ -1046,7 +1045,7 @@ function with the server still running." (funcall eglot--current-flymake-report-fn diags) (setq eglot--unreported-diagnostics nil)) (t - (setq eglot--unreported-diagnostics diags))))) + (setq eglot--unreported-diagnostics (cons t diags)))))) (eglot--debug server "Diagnostics received for unvisited %s" uri))) (cl-defun eglot--register-unregister (server jsonrpc-id things how) @@ -1221,7 +1220,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (setq eglot--current-flymake-report-fn report-fn) ;; Report anything unreported (when eglot--unreported-diagnostics - (funcall report-fn eglot--unreported-diagnostics) + (funcall report-fn (cdr eglot--unreported-diagnostics)) (setq eglot--unreported-diagnostics nil))) (defun eglot-xref-backend () @@ -1598,12 +1597,7 @@ Proceed? " ((server eglot-rls) (_method (eql :window/progress)) &key id done title message &allow-other-keys) "Handle notification window/progress" - (setf (eglot--spinner server) (list id title done message)) - (when (and (equal "Indexing" title) done) - (dolist (buffer (eglot--managed-buffers server)) - (with-current-buffer buffer - (funcall (or eglot--current-flymake-report-fn #'ignore) - eglot--unreported-diagnostics))))) + (setf (eglot--spinner server) (list id title done message))) ;;; cquery-specific From d7864900a58fc4ce335811a69d15a5e954b06000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 27 May 2018 13:17:07 +0100 Subject: [PATCH 190/771] * eglot.el (version): bump to 0.6 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ce131958db4..cd0ee654776 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.5 +;; Version: 0.6 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From 882b571693992b5f2780e97070aa30265eab2a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 27 May 2018 14:17:36 +0100 Subject: [PATCH 191/771] Correctly apply workspace edits in documentchanges form This was breaking M-x eglot-rename for cquery * eglot.el (eglot--apply-workspace-edit): Fix and simplify. --- lisp/progmodes/eglot.el | 55 +++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cd0ee654776..fa12577c694 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1491,39 +1491,30 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (defun eglot--apply-workspace-edit (wedit &optional confirm) "Apply the workspace edit WEDIT. If CONFIRM, ask user first." - (let (prepared) - (cl-destructuring-bind (&key changes documentChanges) - wedit - (cl-loop - for change on documentChanges - do (push (cl-destructuring-bind (&key textDocument edits) change - (cl-destructuring-bind (&key uri version) textDocument - (list (eglot--uri-to-path uri) edits version))) - prepared)) + (cl-destructuring-bind (&key changes documentChanges) wedit + (let ((prepared + (mapcar (eglot--lambda (&key textDocument edits) + (cl-destructuring-bind (&key uri version) textDocument + (list (eglot--uri-to-path uri) edits version))) + documentChanges))) (cl-loop for (uri edits) on changes by #'cddr - do (push (list (eglot--uri-to-path uri) edits) prepared))) - (if (or confirm - (cl-notevery #'find-buffer-visiting - (mapcar #'car prepared))) - (unless (y-or-n-p - (format "[eglot] Server requests to edit %s files.\n %s\n\ -Proceed? " - (length prepared) - (mapconcat #'identity - (mapcar #'car prepared) - "\n "))) - (eglot--error "User cancelled server edit"))) - (unwind-protect - (let (edit) - (while (setq edit (car prepared)) - (cl-destructuring-bind (path edits &optional version) edit - (with-current-buffer (find-file-noselect path) - (eglot--apply-text-edits edits version)) - (pop prepared)))) - (if prepared - (eglot--warn "Caution: edits of files %s failed." - (mapcar #'car prepared)) - (eglot--message "Edit successful!"))))) + do (push (list (eglot--uri-to-path uri) edits) prepared)) + (if (or confirm + (cl-notevery #'find-buffer-visiting + (mapcar #'car prepared))) + (unless (y-or-n-p + (format "[eglot] Server wants to edit:\n %s\n Proceed? " + (mapconcat #'identity (mapcar #'car prepared) "\n "))) + (eglot--error "User cancelled server edit"))) + (unwind-protect + (let (edit) (while (setq edit (car prepared)) + (cl-destructuring-bind (path edits &optional version) edit + (with-current-buffer (find-file-noselect path) + (eglot--apply-text-edits edits version)) + (pop prepared)))) + (if prepared (eglot--warn "Caution: edits of files %s failed." + (mapcar #'car prepared)) + (eglot--message "Edit successful!")))))) (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." From f7e5adc1b1727aa778759e44b4d9b991413c134f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 27 May 2018 14:53:53 +0100 Subject: [PATCH 192/771] Set eglot--versioned-identifier to 0 on didopen Else cquery will rightfully complain about this. * eglot.el (eglot--signal-textDocument/didOpen): Also set eglot--versioned-identifier to 0. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fa12577c694..dde0c22f830 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1181,7 +1181,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." - (setq eglot--recent-changes (cons [] [])) + (setq eglot--recent-changes (cons [] []) eglot--versioned-identifier 0) (eglot--notify (eglot--current-server-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) From 0c80d78b6d4815c8ca202fb6146e7f78721428a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 27 May 2018 15:34:50 +0100 Subject: [PATCH 193/771] Make eglot--recent-changes a simpler list * eglot.el (eglot-server-ready-p): Don't add default method here. (eglot-server-ready-p): Do it here. (eglot--outstanding-edits-p): Remove. (eglot--before-change, eglot--after-change) (eglot--signal-textDocument/didChange): Use eglot--recent-changes as a list. Simplify. (eglot--signal-textDocument/didOpen): Use eglot--recent-changes as a list. --- lisp/progmodes/eglot.el | 55 +++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index dde0c22f830..c48aa2bf83f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -121,9 +121,7 @@ lasted more than that many seconds." (cl-defgeneric eglot-server-ready-p (server what) ;; API "Tell if SERVER is ready for WHAT in current buffer. If it isn't, a deferrable `eglot--async-request' *will* be -deferred to the future." - (:method (_s _what) "Normally ready if no outstanding changes." - (not (eglot--outstanding-edits-p)))) +deferred to the future.") (cl-defgeneric eglot-handle-request (server method id &rest params) "Handle SERVER's METHOD request with ID and PARAMS.") @@ -1117,10 +1115,8 @@ THINGS are either registrations or unregisterations." (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") -(defun eglot--outstanding-edits-p () - "Non-nil if there are outstanding edits." - (cl-plusp (+ (length (car eglot--recent-changes)) - (length (cdr eglot--recent-changes))))) +(defmethod eglot-server-ready-p (_s _what) + "Normally ready if no outstanding changes." (not eglot--recent-changes)) (defvar eglot--change-idle-timer nil "Idle timer for textDocument/didChange.") @@ -1130,19 +1126,20 @@ Records START and END, crucially convert them into LSP (line/char) positions before that information is lost (because the after-change thingy doesn't know if newlines were deleted/added)" - (setf (car eglot--recent-changes) - (vconcat (car eglot--recent-changes) - `[(,(eglot--pos-to-lsp-position start) - ,(eglot--pos-to-lsp-position end))]))) + (when (listp eglot--recent-changes) + (push `(,(eglot--pos-to-lsp-position start) + ,(eglot--pos-to-lsp-position end)) + eglot--recent-changes))) (defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. Records START, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (setf (cdr eglot--recent-changes) - (vconcat (cdr eglot--recent-changes) - `[(,pre-change-length - ,(buffer-substring-no-properties start end))])) + (if (and (listp eglot--recent-changes) + (null (cddr (car eglot--recent-changes)))) + (setf (cddr (car eglot--recent-changes)) + `(,pre-change-length ,(buffer-substring-no-properties start end))) + (setf eglot--recent-changes :emacs-messup)) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) (setq eglot--change-idle-timer (run-with-idle-timer @@ -1151,14 +1148,11 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." - (when (eglot--outstanding-edits-p) + (when eglot--recent-changes (let* ((server (eglot--current-server-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) - (emacs-messup (/= (length (car eglot--recent-changes)) - (length (cdr eglot--recent-changes)))) - (full-sync-p (or (eq sync-kind 1) emacs-messup))) - (when emacs-messup - (eglot--warn "`eglot--recent-changes' messup: %s" eglot--recent-changes)) + (full-sync-p (or (eq sync-kind 1) + (eq :emacs-messup eglot--recent-changes)))) (save-restriction (widen) (eglot--notify @@ -1166,22 +1160,19 @@ Records START, END and PRE-CHANGE-LENGTH locally." (list :textDocument (eglot--VersionedTextDocumentIdentifier) :contentChanges - (if full-sync-p (vector - (list - :text (buffer-substring-no-properties (point-min) - (point-max)))) - (cl-loop for (start-pos end-pos) across (car eglot--recent-changes) - for (len after-text) across (cdr eglot--recent-changes) - vconcat `[,(list :range `(:start ,start-pos :end ,end-pos) - :rangeLength len - :text after-text)]))))) - (setq eglot--recent-changes (cons [] [])) + (if full-sync-p + (vector `(:text ,(buffer-substring-no-properties (point-min) + (point-max)))) + (cl-loop for (beg end len text) in (reverse eglot--recent-changes) + vconcat `[,(list :range `(:start ,beg :end ,end) + :rangeLength len :text text)]))))) + (setq eglot--recent-changes nil) (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." - (setq eglot--recent-changes (cons [] []) eglot--versioned-identifier 0) + (setq eglot--recent-changes nil eglot--versioned-identifier 0) (eglot--notify (eglot--current-server-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) From 7652fd090993f523fcf249fa8f2ed70ccfe784b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 27 May 2018 16:51:15 +0100 Subject: [PATCH 194/771] Be more criterious before running the idle timer * eglot.el (eglot--change-idle-timer): make a defvar-local (eglot--after-change): Only run timer if the buffer is live. --- lisp/progmodes/eglot.el | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c48aa2bf83f..8ab4b496e2d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1118,7 +1118,7 @@ THINGS are either registrations or unregisterations." (defmethod eglot-server-ready-p (_s _what) "Normally ready if no outstanding changes." (not eglot--recent-changes)) -(defvar eglot--change-idle-timer nil "Idle timer for textDocument/didChange.") +(defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") (defun eglot--before-change (start end) "Hook onto `before-change-functions'. @@ -1141,10 +1141,14 @@ Records START, END and PRE-CHANGE-LENGTH locally." `(,pre-change-length ,(buffer-substring-no-properties start end))) (setf eglot--recent-changes :emacs-messup)) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) - (setq eglot--change-idle-timer - (run-with-idle-timer - 0.5 nil (lambda () (eglot--signal-textDocument/didChange) - (setq eglot--change-idle-timer nil))))) + (let ((buf (current-buffer))) + (setq eglot--change-idle-timer + (run-with-idle-timer + 0.5 nil (lambda () (when (buffer-live-p buf) + (with-current-buffer buf + (when eglot--managed-mode + (eglot--signal-textDocument/didChange) + (setq eglot--change-idle-timer nil))))))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." From 1febf627146fdb294bd9e04ef9ce1782a1c1ad27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 27 May 2018 19:45:52 +0100 Subject: [PATCH 195/771] On reconnection, ignore errors of shutting down hung server * eglot.el (eglot, eglot-reconnect): Ignore any errors on shutdown. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8ab4b496e2d..d75499348f8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -397,7 +397,7 @@ INTERACTIVE is t if called interactively." (eglot-reconnect current-server interactive) (when (and current-server (process-live-p (eglot--process current-server))) - (eglot-shutdown current-server)) + (ignore-errors (eglot-shutdown current-server))) (let ((server (eglot--connect project managed-major-mode command @@ -413,7 +413,7 @@ managing `%s' buffers in project `%s'." INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose) t)) (when (process-live-p (eglot--process server)) - (eglot-shutdown server interactive)) + (ignore-errors (eglot-shutdown server interactive))) (eglot--connect (eglot--project server) (eglot--major-mode server) (eglot--contact server) From 9ad9651c4f58b3fb1355cc91e30fae385dbc43e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 28 May 2018 22:30:01 +0100 Subject: [PATCH 196/771] New m-x eglot-stderr-buffer useful for debugging * eglot.el (eglot--make-process): Save stderr buffer in process. (eglot-stderr-buffer): New interactive command. (eglot--mode-line-format): Bind C-mouse-1 to new eglot-stderr-buffer. --- lisp/progmodes/eglot.el | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d75499348f8..f3bf2b7b031 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -233,22 +233,21 @@ DEFERRED request from BUF, to be sent not later than TIMER as ID." "Make a process object from CONTACT. NAME is used to name the the started process or connection. CONTACT is in `eglot'. Returns a process object." - (let* ((buffer (get-buffer-create (format "*%s stdout*" name))) + (let* ((stdout (format "*%s stdout*" name)) stderr (proc (cond ((processp contact) contact) ((integerp (cadr contact)) - (apply #'open-network-stream name buffer contact)) + (apply #'open-network-stream name stdout contact)) (t (make-process - :name name - :command contact - :coding 'no-conversion - :connection-type 'pipe - :stderr (get-buffer-create (format "*%s stderr*" name))))))) - (set-process-buffer proc buffer) - (set-marker (process-mark proc) (with-current-buffer buffer (point-min))) + :name name :command contact :buffer stdout + :coding 'no-conversion :connection-type 'pipe + :stderr (setq stderr (format "*%s stderr*" name))))))) + (process-put proc 'eglot-stderr stderr) + (set-process-buffer proc (get-buffer-create stdout)) + (set-marker (process-mark proc) (with-current-buffer stdout (point-min))) (set-process-filter proc #'eglot--process-filter) (set-process-sentinel proc #'eglot--process-sentinel) - (with-current-buffer buffer + (with-current-buffer stdout (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t))) proc)) @@ -544,6 +543,12 @@ INTERACTIVE is t if called interactively." (when interactive (display-buffer buffer)) buffer)) +(defun eglot-stderr-buffer (server) + "Pop to stderr of SERVER, if it exists, else error." + (interactive (list (eglot--current-server-or-lose))) + (if-let ((b (process-get (eglot--process server) 'eglot-stderr))) + (pop-to-buffer b) (user-error "[eglot] No stderr buffer!"))) + (defun eglot--log-event (server message &optional type) "Log an eglot-related event. SERVER is the current server. MESSAGE is a JSON-like plist. @@ -934,26 +939,25 @@ Uses THING, FACE, DEFS and PREPEND." (when name `(":" ,(eglot--mode-line-props name 'eglot-mode-line - '((mouse-1 eglot-events-buffer "go to events buffer") + '((C-mouse-1 eglot-stderr-buffer "go to stderr buffer") + (mouse-1 eglot-events-buffer "go to events buffer") (mouse-2 eglot-shutdown "quit server") (mouse-3 eglot-reconnect "reconnect to server"))) ,@(when serious-p `("/" ,(eglot--mode-line-props "error" 'compilation-mode-line-fail - '((mouse-1 eglot-events-buffer "go to events buffer") - (mouse-3 eglot-clear-status "clear this status")) + '((mouse-3 eglot-clear-status "clear this status")) (format "An error occured: %s\n" status)))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props (format "%s%s" doing (if detail (format ":%s" detail) "")) - 'compilation-mode-line-run - '((mouse-1 eglot-events-buffer "go to events buffer"))))) + 'compilation-mode-line-run '()))) ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props (format "%d" pending) 'warning - '((mouse-1 eglot-events-buffer "go to events buffer") - (mouse-3 eglot-clear-status "clear this status")) + '((mouse-3 eglot-forget-pending-continuations + "forget these continuations")) (format "%d pending requests\n" pending))))))))) (add-to-list 'mode-line-misc-info From 84189937d42a581a1bfa44252caf9b3cb1d6b4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 28 May 2018 23:07:56 +0100 Subject: [PATCH 197/771] More yak shaving * eglot.el (eglot--with-live-buffer, eglot--widening): New macros. (eglot--lambda): Move up here. (eglot--process-filter): Simplify with eglot--with-live-buffer. (eglot--async-request): Simplify with eglot--with-live-buffer. (eglot--TextDocumentItem): Simplify with eglot--widening. (eglot--signal-textDocument/didChange, eglot--apply-text-edits): Simplify with eglot--widening. --- lisp/progmodes/eglot.el | 205 ++++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 102 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f3bf2b7b031..6a7ba6bad4c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -118,6 +118,22 @@ lasted more than that many seconds." ;;; API (WORK-IN-PROGRESS!) ;;; +(cl-defmacro eglot--with-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--lambda (cl-lambda-list &body body) + "Make a unary function of ARG, a plist-like JSON object. +CL-LAMBDA-LIST destructures ARGS before running BODY." + (declare (indent 1) (debug (sexp &rest form))) + (let ((e (gensym "eglot--lambda-elem"))) + `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) + +(cl-defmacro eglot--widening (&rest body) + "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) + `(save-excursion (save-restriction (widen) ,@body))) + (cl-defgeneric eglot-server-ready-p (server what) ;; API "Tell if SERVER is ready for WHAT in current buffer. If it isn't, a deferrable `eglot--async-request' *will* be @@ -464,67 +480,64 @@ INTERACTIVE is t if called interactively." (defun eglot--process-filter (proc string) "Called when new data STRING has arrived for PROC." - (when (buffer-live-p (process-buffer proc)) - (with-current-buffer (process-buffer proc) - (let ((inhibit-read-only t) - (expected-bytes (process-get proc 'eglot-expected-bytes))) - ;; Insert the text, advancing the process marker. - ;; - (save-excursion - (goto-char (process-mark proc)) - (insert string) - (set-marker (process-mark proc) (point))) - ;; Loop (more than one message might have arrived) - ;; - (unwind-protect - (let (done) - (while (not done) - (cond - ((not expected-bytes) - ;; Starting a new message - ;; - (setq expected-bytes - (and (search-forward-regexp - "\\(?:.*: .*\r\n\\)*Content-Length: \ + (eglot--with-live-buffer (process-buffer proc) + (let ((expected-bytes (process-get proc 'eglot-expected-bytes)) + (inhibit-read-only t) done) + ;; Insert the text, advancing the process marker. + ;; + (save-excursion + (goto-char (process-mark proc)) + (insert string) + (set-marker (process-mark proc) (point))) + ;; Loop (more than one message might have arrived) + ;; + (unwind-protect + (while (not done) + (cond ((not expected-bytes) + ;; Starting a new message + ;; + (setq expected-bytes + (and (search-forward-regexp + "\\(?:.*: .*\r\n\\)*Content-Length: \ *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" - (+ (point) 100) - t) - (string-to-number (match-string 1)))) - (unless expected-bytes - (setq done :waiting-for-new-message))) - (t - ;; Attempt to complete a message body - ;; - (let ((available-bytes (- (position-bytes (process-mark proc)) - (position-bytes (point))))) - (cond - ((>= available-bytes - expected-bytes) - (let* ((message-end (byte-to-position - (+ (position-bytes (point)) - expected-bytes)))) - (unwind-protect - (save-restriction - (narrow-to-region (point) message-end) - (let* ((json-object-type 'plist) - (json-message (json-read))) - ;; Process content in another buffer, - ;; shielding buffer from tamper - ;; - (with-temp-buffer - (eglot--server-receive - (process-get proc 'eglot-server) - json-message)))) - (goto-char message-end) - (delete-region (point-min) (point)) - (setq expected-bytes nil)))) - (t - ;; Message is still incomplete - ;; - (setq done :waiting-for-more-bytes-in-this-message)))))))) - ;; Saved parsing state for next visit to this filter - ;; - (process-put proc 'eglot-expected-bytes expected-bytes)))))) + (+ (point) 100) + t) + (string-to-number (match-string 1)))) + (unless expected-bytes + (setq done :waiting-for-new-message))) + (t + ;; Attempt to complete a message body + ;; + (let ((available-bytes (- (position-bytes (process-mark proc)) + (position-bytes (point))))) + (cond + ((>= available-bytes + expected-bytes) + (let* ((message-end (byte-to-position + (+ (position-bytes (point)) + expected-bytes)))) + (unwind-protect + (save-restriction + (narrow-to-region (point) message-end) + (let* ((json-object-type 'plist) + (json-message (json-read))) + ;; Process content in another buffer, + ;; shielding buffer from tamper + ;; + (with-temp-buffer + (eglot--server-receive + (process-get proc 'eglot-server) + json-message)))) + (goto-char message-end) + (delete-region (point-min) (point)) + (setq expected-bytes nil)))) + (t + ;; Message is still incomplete + ;; + (setq done :waiting-for-more-bytes-in-this-message))))))) + ;; Saved parsing state for next visit to this filter + ;; + (process-put proc 'eglot-expected-bytes expected-bytes))))) (defun eglot-events-buffer (server &optional interactive) "Display events buffer for current LSP SERVER. @@ -631,11 +644,6 @@ originated." (eglot--debug server `(:maybe-run-deferred ,(mapcar #'caddr actions))) (mapc #'funcall (mapcar #'car actions)))) -(cl-defmacro eglot--lambda (cl-lambda-list &body body) - (declare (indent 1) (debug (sexp &rest form))) - (let ((e (gensym "eglot--lambda-elem"))) - `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) - (defvar-local eglot--next-request-id 0 "ID for next `eglot--async-request'.") (cl-defun eglot--async-request (server @@ -652,7 +660,7 @@ nullary TIMEOUT-FN. If DEFERRED, maybe defer request to the future, or to never at all, in case a new request with identical DEFERRED and for the same buffer overrides it (however, if that happens, the original timer keeps counting). Return (ID TIMER)." - (pcase-let* ( (buf (current-buffer)) (pos (point-marker)) + (pcase-let* ( (buf (current-buffer)) (`(,_ ,timer ,old-id) (and deferred (gethash (list deferred buf) (eglot--deferred-actions server)))) @@ -675,12 +683,9 @@ happens, the original timer keeps counting). Return (ID TIMER)." ;; Also, if it's the first deferring for this id, inform the log (eglot--debug server `(:deferring ,method :id ,id :params ,params))) (puthash (list deferred buf) - (list (lambda () (when (buffer-live-p buf) - (with-current-buffer buf - (save-excursion - (goto-char pos) - (apply #'eglot--async-request server - method params args))))) + (list (lambda () (eglot--with-live-buffer buf + (apply #'eglot--async-request server + method params args))) (or timer (funcall make-timer)) id) (eglot--deferred-actions server)) (cl-return-from eglot--async-request nil))) @@ -741,7 +746,7 @@ DEFERRED is passed to `eglot--async-request', which see." ,@(when error `(:error ,error))))) -;;; Helpers +;;; Helpers (move these to API?) ;;; (defun eglot--error (format &rest args) "Error out with FORMAT with ARGS." @@ -1107,9 +1112,8 @@ THINGS are either registrations or unregisterations." (match-string 1 (symbol-name major-mode)) "unknown") :text - (save-restriction - (widen) - (buffer-substring-no-properties (point-min) (point-max)))))) + (eglot--widening + (buffer-substring-no-properties (point-min) (point-max)))))) (defun eglot--TextDocumentPositionParams () "Compute TextDocumentPositionParams." @@ -1148,11 +1152,10 @@ Records START, END and PRE-CHANGE-LENGTH locally." (let ((buf (current-buffer))) (setq eglot--change-idle-timer (run-with-idle-timer - 0.5 nil (lambda () (when (buffer-live-p buf) - (with-current-buffer buf - (when eglot--managed-mode - (eglot--signal-textDocument/didChange) - (setq eglot--change-idle-timer nil))))))))) + 0.5 nil (lambda () (eglot--with-live-buffer buf + (when eglot--managed-mode + (eglot--signal-textDocument/didChange) + (setq eglot--change-idle-timer nil)))))))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." @@ -1161,19 +1164,19 @@ Records START, END and PRE-CHANGE-LENGTH locally." (sync-kind (eglot--server-capable :textDocumentSync)) (full-sync-p (or (eq sync-kind 1) (eq :emacs-messup eglot--recent-changes)))) - (save-restriction - (widen) - (eglot--notify - server :textDocument/didChange - (list - :textDocument (eglot--VersionedTextDocumentIdentifier) - :contentChanges - (if full-sync-p - (vector `(:text ,(buffer-substring-no-properties (point-min) - (point-max)))) - (cl-loop for (beg end len text) in (reverse eglot--recent-changes) - vconcat `[,(list :range `(:start ,beg :end ,end) - :rangeLength len :text text)]))))) + (eglot--notify + server :textDocument/didChange + (list + :textDocument (eglot--VersionedTextDocumentIdentifier) + :contentChanges + (if full-sync-p + (vector `(:text ,(eglot--widening + (buffer-substring-no-properties (point-min) + (point-max))))) + (cl-loop for (beg end len text) in (reverse eglot--recent-changes) + vconcat `[,(list :range `(:start ,beg :end ,end) + :rangeLength len :text text)])))) + (setq eglot--recent-changes nil) (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) @@ -1478,14 +1481,12 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (save-restriction - (widen) - (save-excursion - (mapc (pcase-lambda (`(,newText ,beg . ,end)) - (goto-char beg) (delete-region beg end) (insert newText)) - (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) - edits)))) + (eglot--widening + (mapc (pcase-lambda (`(,newText ,beg . ,end)) + (goto-char beg) (delete-region beg end) (insert newText)) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) From db5dc348a1b12a124de05be05cbbbce412cf5c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 30 May 2018 03:20:54 +0100 Subject: [PATCH 198/771] * eglot.el (version): bump to 0.7 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6a7ba6bad4c..cc5649fa382 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.6 +;; Version: 0.7 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From 07d71f4ec75cc60634d36fff25ffc362be3dad72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 1 Jun 2018 14:12:54 +0100 Subject: [PATCH 199/771] Prevent possible cquery choke on :initializationoptions Hopefully help debug https://github.com/joaotavora/eglot/issues/10. * eglot.el (eglot-initialization-options): Use `list' --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cc5649fa382..a991cb1f785 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1600,8 +1600,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Passes through required cquery initialization options" (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) - (vector :cacheDirectory (file-name-as-directory cache) - :progressReportFrequencyMs -1))) + (list :cacheDirectory (file-name-as-directory cache) + :progressReportFrequencyMs -1))) (cl-defmethod eglot-handle-notification ((_server eglot-cquery) (_method (eql :$cquery/progress)) From ee6ab89666704232f744d6262040979b0142c44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 1 Jun 2018 14:43:30 +0100 Subject: [PATCH 200/771] Add melpa badge * README.mdown: Now in MELPA too --- lisp/progmodes/eglot.el | 104 +++++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a991cb1f785..3dfe1597621 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -908,12 +908,15 @@ that case, also signal textDocument/didOpen." (put 'eglot--mode-line-format 'risky-local-variable t) -(defun eglot--mode-line-call (what) +(defun eglot--mouse-call (what) "Make an interactive lambda for calling WHAT from mode-line." (lambda (event) (interactive "e") - (with-selected-window (posn-window (event-start event)) - (call-interactively what)))) + (let ((start (event-start event))) (with-selected-window (posn-window start) + (save-excursion + (goto-char (or (posn-point start) + (point))) + (call-interactively what)))))) (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. @@ -921,7 +924,7 @@ Uses THING, FACE, DEFS and PREPEND." (cl-loop with map = (make-sparse-keymap) for (elem . rest) on defs for (key def help) = elem - do (define-key map `[mode-line ,key] (eglot--mode-line-call def)) + do (define-key map `[mode-line ,key] (eglot--mouse-call def)) concat (format "%s: %s" key help) into blurb when rest concat "\n" into blurb finally (return `(:propertize ,thing @@ -968,6 +971,41 @@ Uses THING, FACE, DEFS and PREPEND." (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) + +;; A horrible hack of Flymake's insufficient API that must go into +;; Emacs master, or better, 26.2 +(cl-defstruct (eglot--diag (:include flymake--diag) + (:constructor eglot--make-diag + (buffer beg end type text props))) + props) +(advice-add 'flymake--highlight-line :after + (lambda (diag) + (when (cl-typep diag 'eglot--diag) + (let ((ov (cl-find diag + (overlays-at (flymake-diagnostic-beg diag)) + :key (lambda (ov) + (overlay-get ov 'flymake-diagnostic))))) + (cl-loop for (key . value) in (eglot--diag-props diag) + do (overlay-put ov key value))))) + '((name . eglot-hacking-in-some-per-diag-overlay-properties))) + + +(defun eglot--overlay-diag-props () + `((mouse-face . highlight) + (help-echo . (lambda (window _ov pos) + (with-selected-window window + (concat (mapconcat + #'flymake-diagnostic-text + (flymake-diagnostics pos) + "\n") + "\nmouse-1: Get LSP code actions")))) + (keymap . ,(let ((map (make-sparse-keymap))) + (define-key map [mouse-1] + (eglot--mouse-call 'eglot-get-code-actions)) + map)))) + + + ;;; Protocol implementation (Requests, notifications, etc) ;;; @@ -1037,16 +1075,18 @@ function with the server still running." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity _group + collect (cl-destructuring-bind (&key range ((:severity sev)) _group _code source message) diag-spec + (setq message (concat source ": " message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (flymake-make-diagnostic (current-buffer) - beg end - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message)))) + (eglot--make-diag (current-buffer) beg end + (cond ((<= sev 1) ':error) + ((= sev 2) ':warning) + (t ':note)) + message (cons + `(eglot-lsp-diag . ,diag-spec) + (eglot--overlay-diag-props))))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1528,6 +1568,44 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :newName ,newname)) current-prefix-arg)) + +(defun eglot-get-code-actions (&optional beg end) + "Get code actions between BEG and END." + (interactive + (let (diags) + (cond ((region-active-p) (list (region-beginning) (region-end))) + ((setq diags (flymake-diagnostics (point))) + (list (cl-reduce #'min (mapcar #'flymake-diagnostic-beg diags)) + (cl-reduce #'max (mapcar #'flymake-diagnostic-end diags)))) + (t (list (point-min) (point-max)))))) + (let* ((actions (eglot--request + (eglot--current-server-or-lose) + :textDocument/codeAction + (list :textDocument (eglot--TextDocumentIdentifier) + :range (list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end)) + :context + `(:diagnostics + [,@(mapcar (lambda (diag) + (cdr (assoc 'eglot-lsp-diag + (eglot--diag-props diag)))) + (cl-remove-if-not + (lambda (diag) (cl-typep diag 'eglot--diag)) + (flymake-diagnostics beg end)))])))) + (menu (let ((map (make-sparse-keymap))) + (mapc (eglot--lambda (&key title command _arguments) + (define-key map (vector (intern command)) + `(,title dummy))) + actions) + (setq map `(keymap "Code actions here:" ,@(cdr map))))) + (command-sym (car + (if (listp last-nonmenu-event) + (x-popup-menu last-nonmenu-event menu) + (tmm-prompt menu)))) + + ) + (message "would be applying %S" command-sym))) + ;;; Dynamic registration ;;; @@ -1601,7 +1679,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) (list :cacheDirectory (file-name-as-directory cache) - :progressReportFrequencyMs -1))) + :progressReportFrequencyMs -1 + :discoverSystemIncludes :json-false + :enableIndexOnDidChange t))) (cl-defmethod eglot-handle-notification ((_server eglot-cquery) (_method (eql :$cquery/progress)) From 5a8d94c76834d510a86e35588113c38355204068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 1 Jun 2018 16:09:19 +0100 Subject: [PATCH 201/771] Revert an unfinished feature that made it to the last commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit ee6ab89666704232f744d6262040979b0142c44b Author: João Távora Date: Fri Jun 1 14:43:30 2018 +0100 Add MELPA badge --- lisp/progmodes/eglot.el | 104 +++++----------------------------------- 1 file changed, 12 insertions(+), 92 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3dfe1597621..a991cb1f785 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -908,15 +908,12 @@ that case, also signal textDocument/didOpen." (put 'eglot--mode-line-format 'risky-local-variable t) -(defun eglot--mouse-call (what) +(defun eglot--mode-line-call (what) "Make an interactive lambda for calling WHAT from mode-line." (lambda (event) (interactive "e") - (let ((start (event-start event))) (with-selected-window (posn-window start) - (save-excursion - (goto-char (or (posn-point start) - (point))) - (call-interactively what)))))) + (with-selected-window (posn-window (event-start event)) + (call-interactively what)))) (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. @@ -924,7 +921,7 @@ Uses THING, FACE, DEFS and PREPEND." (cl-loop with map = (make-sparse-keymap) for (elem . rest) on defs for (key def help) = elem - do (define-key map `[mode-line ,key] (eglot--mouse-call def)) + do (define-key map `[mode-line ,key] (eglot--mode-line-call def)) concat (format "%s: %s" key help) into blurb when rest concat "\n" into blurb finally (return `(:propertize ,thing @@ -971,41 +968,6 @@ Uses THING, FACE, DEFS and PREPEND." (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) - -;; A horrible hack of Flymake's insufficient API that must go into -;; Emacs master, or better, 26.2 -(cl-defstruct (eglot--diag (:include flymake--diag) - (:constructor eglot--make-diag - (buffer beg end type text props))) - props) -(advice-add 'flymake--highlight-line :after - (lambda (diag) - (when (cl-typep diag 'eglot--diag) - (let ((ov (cl-find diag - (overlays-at (flymake-diagnostic-beg diag)) - :key (lambda (ov) - (overlay-get ov 'flymake-diagnostic))))) - (cl-loop for (key . value) in (eglot--diag-props diag) - do (overlay-put ov key value))))) - '((name . eglot-hacking-in-some-per-diag-overlay-properties))) - - -(defun eglot--overlay-diag-props () - `((mouse-face . highlight) - (help-echo . (lambda (window _ov pos) - (with-selected-window window - (concat (mapconcat - #'flymake-diagnostic-text - (flymake-diagnostics pos) - "\n") - "\nmouse-1: Get LSP code actions")))) - (keymap . ,(let ((map (make-sparse-keymap))) - (define-key map [mouse-1] - (eglot--mouse-call 'eglot-get-code-actions)) - map)))) - - - ;;; Protocol implementation (Requests, notifications, etc) ;;; @@ -1075,18 +1037,16 @@ function with the server still running." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range ((:severity sev)) _group + collect (cl-destructuring-bind (&key range severity _group _code source message) diag-spec - (setq message (concat source ": " message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (eglot--make-diag (current-buffer) beg end - (cond ((<= sev 1) ':error) - ((= sev 2) ':warning) - (t ':note)) - message (cons - `(eglot-lsp-diag . ,diag-spec) - (eglot--overlay-diag-props))))) + (flymake-make-diagnostic (current-buffer) + beg end + (cond ((<= severity 1) :error) + ((= severity 2) :warning) + (t :note)) + (concat source ": " message)))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1568,44 +1528,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :newName ,newname)) current-prefix-arg)) - -(defun eglot-get-code-actions (&optional beg end) - "Get code actions between BEG and END." - (interactive - (let (diags) - (cond ((region-active-p) (list (region-beginning) (region-end))) - ((setq diags (flymake-diagnostics (point))) - (list (cl-reduce #'min (mapcar #'flymake-diagnostic-beg diags)) - (cl-reduce #'max (mapcar #'flymake-diagnostic-end diags)))) - (t (list (point-min) (point-max)))))) - (let* ((actions (eglot--request - (eglot--current-server-or-lose) - :textDocument/codeAction - (list :textDocument (eglot--TextDocumentIdentifier) - :range (list :start (eglot--pos-to-lsp-position beg) - :end (eglot--pos-to-lsp-position end)) - :context - `(:diagnostics - [,@(mapcar (lambda (diag) - (cdr (assoc 'eglot-lsp-diag - (eglot--diag-props diag)))) - (cl-remove-if-not - (lambda (diag) (cl-typep diag 'eglot--diag)) - (flymake-diagnostics beg end)))])))) - (menu (let ((map (make-sparse-keymap))) - (mapc (eglot--lambda (&key title command _arguments) - (define-key map (vector (intern command)) - `(,title dummy))) - actions) - (setq map `(keymap "Code actions here:" ,@(cdr map))))) - (command-sym (car - (if (listp last-nonmenu-event) - (x-popup-menu last-nonmenu-event menu) - (tmm-prompt menu)))) - - ) - (message "would be applying %S" command-sym))) - ;;; Dynamic registration ;;; @@ -1679,9 +1601,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) (list :cacheDirectory (file-name-as-directory cache) - :progressReportFrequencyMs -1 - :discoverSystemIncludes :json-false - :enableIndexOnDidChange t))) + :progressReportFrequencyMs -1))) (cl-defmethod eglot-handle-notification ((_server eglot-cquery) (_method (eql :$cquery/progress)) From 0e3d15f51c0a06751441c9392db0676a25ddf968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 1 Jun 2018 16:59:00 +0100 Subject: [PATCH 202/771] New command m-x eglot-code-actions Also available when left-clicking diagnostics. * README.md: Mention eglot-code-actions. Slightly rewrite differences to lsp-mode. * eglot.el (eglot-code-actions): New command. (eglot-handle-notification :textDocument/publishDiagnostics): Use eglot--make-diag and eglot--overlay-diag-props. (eglot--mode-line-props): Use eglot--mouse-call. (eglot--mouse-call): Renamed from eglot--mode-line-call. (eglot-client-capabilities): List :executeCommand and :codeAction as capabilities. (eglot--diag, advice-add flymake--highlight-line): Horrible hack. (eglot--overlay-diag-props): Horrible hack. --- lisp/progmodes/eglot.el | 108 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a991cb1f785..8437d8cc8a3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -155,6 +155,8 @@ deferred to the future.") (list :workspace (list :applyEdit t + :executeCommand `(:dynamicRegistration :json-false) + :codeAction `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges :json-false) :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) @@ -908,12 +910,15 @@ that case, also signal textDocument/didOpen." (put 'eglot--mode-line-format 'risky-local-variable t) -(defun eglot--mode-line-call (what) +(defun eglot--mouse-call (what) "Make an interactive lambda for calling WHAT from mode-line." (lambda (event) (interactive "e") - (with-selected-window (posn-window (event-start event)) - (call-interactively what)))) + (let ((start (event-start event))) (with-selected-window (posn-window start) + (save-excursion + (goto-char (or (posn-point start) + (point))) + (call-interactively what)))))) (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. @@ -921,7 +926,7 @@ Uses THING, FACE, DEFS and PREPEND." (cl-loop with map = (make-sparse-keymap) for (elem . rest) on defs for (key def help) = elem - do (define-key map `[mode-line ,key] (eglot--mode-line-call def)) + do (define-key map `[mode-line ,key] (eglot--mouse-call def)) concat (format "%s: %s" key help) into blurb when rest concat "\n" into blurb finally (return `(:propertize ,thing @@ -968,6 +973,39 @@ Uses THING, FACE, DEFS and PREPEND." (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) + +;; FIXME: A horrible hack of Flymake's insufficient API that must go +;; into Emacs master, or better, 26.2 +(cl-defstruct (eglot--diag (:include flymake--diag) + (:constructor eglot--make-diag + (buffer beg end type text props))) + props) + +(advice-add 'flymake--highlight-line :after + (lambda (diag) + (when (cl-typep diag 'eglot--diag) + (let ((ov (cl-find diag + (overlays-at (flymake-diagnostic-beg diag)) + :key (lambda (ov) + (overlay-get ov 'flymake-diagnostic))))) + (cl-loop for (key . value) in (eglot--diag-props diag) + do (overlay-put ov key value))))) + '((name . eglot-hacking-in-some-per-diag-overlay-properties))) + + +(defun eglot--overlay-diag-props () + `((mouse-face . highlight) + (help-echo . (lambda (window _ov pos) + (with-selected-window window + (mapconcat + #'flymake-diagnostic-text + (flymake-diagnostics pos) + "\n")))) + (keymap . ,(let ((map (make-sparse-keymap))) + (define-key map [mouse-1] + (eglot--mouse-call 'eglot-code-actions)) + map)))) + ;;; Protocol implementation (Requests, notifications, etc) ;;; @@ -1037,16 +1075,18 @@ function with the server still running." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range severity _group + collect (cl-destructuring-bind (&key range ((:severity sev)) _group _code source message) diag-spec + (setq message (concat source ": " message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (flymake-make-diagnostic (current-buffer) - beg end - (cond ((<= severity 1) :error) - ((= severity 2) :warning) - (t :note)) - (concat source ": " message)))) + (eglot--make-diag (current-buffer) beg end + (cond ((<= sev 1) ':error) + ((= sev 2) ':warning) + (t ':note)) + message (cons + `(eglot-lsp-diag . ,diag-spec) + (eglot--overlay-diag-props))))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1528,6 +1568,52 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :newName ,newname)) current-prefix-arg)) + +(defun eglot-code-actions (&optional beg end) + "Get and offer to execute code actions between BEG and END." + (interactive + (let (diags) + (cond ((region-active-p) (list (region-beginning) (region-end))) + ((setq diags (flymake-diagnostics (point))) + (list (cl-reduce #'min (mapcar #'flymake-diagnostic-beg diags)) + (cl-reduce #'max (mapcar #'flymake-diagnostic-end diags)))) + (t (list (point-min) (point-max)))))) + (unless (eglot--server-capable :codeActionProvider) + (eglot--error "Server can't execute code actions!")) + (let* ((server (eglot--current-server-or-lose)) + (actions (eglot--request + server + :textDocument/codeAction + (list :textDocument (eglot--TextDocumentIdentifier) + :range (list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end)) + :context + `(:diagnostics + [,@(mapcar (lambda (diag) + (cdr (assoc 'eglot-lsp-diag + (eglot--diag-props diag)))) + (cl-remove-if-not + (lambda (diag) (cl-typep diag 'eglot--diag)) + (flymake-diagnostics beg end)))])))) + (menu-items (mapcar (eglot--lambda (&key title command arguments) + `(,title . (:command ,command :arguments ,arguments))) + actions)) + (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) + (command-and-args + (and menu + (if (listp last-nonmenu-event) + (x-popup-menu last-nonmenu-event menu) + (let ((never-mind (gensym)) retval) + (setcdr (cadr menu) + (cons `("never mind..." . ,never-mind) (cdadr menu))) + (if (eq (setq retval (tmm-prompt menu)) never-mind) + (keyboard-quit) + retval)))))) + (if command-and-args + (eglot--request server :workspace/executeCommand command-and-args) + (eglot--message "No code actions here")))) + + ;;; Dynamic registration ;;; From 8429c2c2feadf517aae798fa561b5c357cb1415e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 1 Jun 2018 17:04:17 +0100 Subject: [PATCH 203/771] Explicitly trigger eldoc after workspace edits It's usually a nice thing to do. * eglot.el (eglot--apply-workspace-edit): Call eglot-eldoc-function. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8437d8cc8a3..b133ffbbb20 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1216,7 +1216,6 @@ Records START, END and PRE-CHANGE-LENGTH locally." (cl-loop for (beg end len text) in (reverse eglot--recent-changes) vconcat `[,(list :range `(:start ,beg :end ,end) :rangeLength len :text text)])))) - (setq eglot--recent-changes nil) (setf (eglot--spinner server) (list nil :textDocument/didChange t)) (eglot--call-deferred server)))) @@ -1554,6 +1553,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (pop prepared)))) (if prepared (eglot--warn "Caution: edits of files %s failed." (mapcar #'car prepared)) + (eglot-eldoc-function) (eglot--message "Edit successful!")))))) (defun eglot-rename (newname) From 7b040d4d1866b2be31ca4809b1bbb341921c8f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 1 Jun 2018 17:52:00 +0100 Subject: [PATCH 204/771] Fix completionitem/resolve Should fix interoperation with company-quickhelp. * eglot.el (eglot-completion-at-point): Correctly pass properties to completionItem/resolve. --- lisp/progmodes/eglot.el | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b133ffbbb20..3c842976344 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1372,7 +1372,9 @@ DUMMY is ignored" (mapcar (eglot--lambda (&rest all &key label insertText &allow-other-keys) (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) insert)) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) @@ -1391,13 +1393,15 @@ DUMMY is ignored" (or (get-text-property 0 :sortText b) ""))))) :company-doc-buffer (lambda (obj) - (let ((documentation - (or (get-text-property 0 :documentation obj) - (and (eglot--server-capable :completionProvider - :resolveProvider) - (plist-get (eglot--request server :completionItem/resolve - (text-properties-at 0 obj)) - :documentation))))) + (let* ((documentation + (or (get-text-property 0 :documentation obj) + (and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get + (eglot--request server :completionItem/resolve + (get-text-property + 0 'eglot--lsp-completion obj)) + :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") (insert (eglot--format-markup documentation)) From 7a5eb10f35d1cb45b8ae0b467ca938108264250f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 1 Jun 2018 17:58:00 +0100 Subject: [PATCH 205/771] * eglot.el (version): bump to 0.8 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3c842976344..6193b93e6dc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.7 +;; Version: 0.8 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From 172c58f304292fbf4801dc31a10ede9f8e3198bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 2 Jun 2018 00:58:10 +0100 Subject: [PATCH 206/771] * eglot.el (eglot-eldoc-function): remove spurious log message --- lisp/progmodes/eglot.el | 1 - 1 file changed, 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 31ef081bd07..64d06a3aa63 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1126,7 +1126,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless sig-showing (when-buffer-window (when-let (info (eglot--hover-info contents range)) - (eglot--message "OK so info is %S and %S" info (null info)) (eldoc-message info))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) From 77a306061053dac632a67f68ab2c0eb4daf28397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 2 Jun 2018 01:06:07 +0100 Subject: [PATCH 207/771] Fix another merge-related bug in eglot-eldoc-function * eglot.el (eglot-eldoc-function): Correctly destructure eglot--range-region. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 64d06a3aa63..3f82c893fd6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1138,7 +1138,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-buffer-window (mapcar (jsonrpc-lambda (&key range _kind _role) - (pcase-let ((`(,beg ,end) + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'highlight) From b1b6be0c3ac20d9dd6910603849c755767869b69 Mon Sep 17 00:00:00 2001 From: brotzeit Date: Mon, 4 Jun 2018 12:50:01 +0200 Subject: [PATCH 208/771] Fix typos Close https://github.com/joaotavora/eglot/issues/13. * eglot.el (eglot--all-major-modes) (eglot--notify, eglot--xref-reset-known-symbols): Fix typos. --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6193b93e6dc..de8d990f3a0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -270,7 +270,7 @@ CONTACT is in `eglot'. Returns a process object." proc)) (defun eglot--all-major-modes () - "Return all know major modes." + "Return all known major modes." (let ((retval)) (mapatoms (lambda (sym) (when (plist-member (symbol-plist sym) 'derived-mode-parent) @@ -737,7 +737,7 @@ DEFERRED is passed to `eglot--async-request', which see." (cadr res))) (cl-defun eglot--notify (server method params) - "Notify SERVER of something, don't expect a reply.e" + "Notify SERVER of something, don't expect a reply." (eglot--send server `(:jsonrpc "2.0" :method ,method :params ,params))) (cl-defun eglot--reply (server id &key result error) @@ -1272,7 +1272,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (defun eglot--xref-reset-known-symbols (&rest _dummy) "Reset `eglot--xref-reset-known-symbols'. -DUMMY is ignored" +DUMMY is ignored." (setq eglot--xref-known-symbols nil)) (advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) From 6bda52d1ac5b90a2cbf402bcc2565f00249467da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 4 Jun 2018 12:49:28 +0100 Subject: [PATCH 209/771] Support purposedly ignoring a server capability * eglot.el (eglot-ignored-server-capabilites): New defcustom. (eglot--server-capable): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/12 --- lisp/progmodes/eglot.el | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index de8d990f3a0..0869bd52cbf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -816,16 +816,26 @@ If optional MARKER, return a marker instead" (ignore-errors (funcall mode)) (insert string) (font-lock-ensure) (buffer-string)))) +(defcustom eglot-ignored-server-capabilites (list) + "LSP server capabilities that Eglot could use, but won't. +You could add, for instance, the symbol +`:documentHighlightProvider' to prevent automatic highlighting +under cursor." + :type '(repeat symbol)) + (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." - (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) - then (cadr probe) - for feat in feats - for probe = (plist-member caps feat) - if (not probe) do (cl-return nil) - if (eq (cadr probe) t) do (cl-return t) - if (eq (cadr probe) :json-false) do (cl-return nil) - finally (cl-return (or probe t)))) + (unless (cl-some (lambda (feat) + (memq feat eglot-ignored-server-capabilites)) + feats) + (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) + then (cadr probe) + for feat in feats + for probe = (plist-member caps feat) + if (not probe) do (cl-return nil) + if (eq (cadr probe) t) do (cl-return t) + if (eq (cadr probe) :json-false) do (cl-return nil) + finally (cl-return (or probe t))))) (defun eglot--range-region (range &optional markers) "Return region (BEG . END) that represents LSP RANGE. From 24898b7d6056b856a22892ed816bfd161e4ade5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 5 Jun 2018 17:26:38 +0100 Subject: [PATCH 210/771] Cleanup the flymake 26.1 hack slightly * eglot.el (eglot-handle-notification): Use proper flymake diagnostic types. (eglot-code-actions): Use eglot--diag-data. (eglot--make-diag, eglot--diag-data): New aliases to `flymake-diagnostic-data' and `flymake-make-diagnostic'. (eglot-error eglot-warning eglot-note) (dolist eglot-error eglot-warning eglot-note): put flymake-overlay-control in these. (eglot-error eglot-warning eglot-note): put corresponding flymake-category. (horrible hack at the end): Move the Flymake 26.1 hack here. --- lisp/progmodes/eglot.el | 92 +++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0869bd52cbf..66ba03a6bd3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -983,38 +983,20 @@ Uses THING, FACE, DEFS and PREPEND." (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) - -;; FIXME: A horrible hack of Flymake's insufficient API that must go -;; into Emacs master, or better, 26.2 -(cl-defstruct (eglot--diag (:include flymake--diag) - (:constructor eglot--make-diag - (buffer beg end type text props))) - props) +(put 'eglot-note 'flymake-category 'flymake-note) +(put 'eglot-warning 'flymake-category 'flymake-warning) +(put 'eglot-error 'flymake-category 'flymake-error) -(advice-add 'flymake--highlight-line :after - (lambda (diag) - (when (cl-typep diag 'eglot--diag) - (let ((ov (cl-find diag - (overlays-at (flymake-diagnostic-beg diag)) - :key (lambda (ov) - (overlay-get ov 'flymake-diagnostic))))) - (cl-loop for (key . value) in (eglot--diag-props diag) - do (overlay-put ov key value))))) - '((name . eglot-hacking-in-some-per-diag-overlay-properties))) +(defalias 'eglot--make-diag 'flymake-make-diagnostic) +(defalias 'eglot--diag-data 'flymake-diagnostic-data) - -(defun eglot--overlay-diag-props () - `((mouse-face . highlight) - (help-echo . (lambda (window _ov pos) - (with-selected-window window - (mapconcat - #'flymake-diagnostic-text - (flymake-diagnostics pos) - "\n")))) - (keymap . ,(let ((map (make-sparse-keymap))) - (define-key map [mouse-1] - (eglot--mouse-call 'eglot-code-actions)) - map)))) +(dolist (type '(eglot-error eglot-warning eglot-note)) + (put type 'flymake-overlay-control + `((mouse-face . highlight) + (keymap . ,(let ((map (make-sparse-keymap))) + (define-key map [mouse-1] + (eglot--mouse-call 'eglot-code-actions)) + map))))) ;;; Protocol implementation (Requests, notifications, etc) @@ -1091,12 +1073,10 @@ function with the server still running." (setq message (concat source ": " message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (eglot--make-diag (current-buffer) beg end - (cond ((<= sev 1) ':error) - ((= sev 2) ':warning) - (t ':note)) - message (cons - `(eglot-lsp-diag . ,diag-spec) - (eglot--overlay-diag-props))))) + (cond ((<= sev 1) 'eglot-error) + ((= sev 2) 'eglot-warning) + (t 'eglot-note)) + message `((eglot-lsp-diag . ,diag-spec))))) into diags finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags) @@ -1605,10 +1585,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." `(:diagnostics [,@(mapcar (lambda (diag) (cdr (assoc 'eglot-lsp-diag - (eglot--diag-props diag)))) - (cl-remove-if-not - (lambda (diag) (cl-typep diag 'eglot--diag)) - (flymake-diagnostics beg end)))])))) + (eglot--diag-data diag)))) + (flymake-diagnostics beg end))])))) (menu-items (mapcar (eglot--lambda (&key title command arguments) `(,title . (:command ,command :arguments ,arguments))) actions)) @@ -1718,6 +1696,40 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." &key _uri _symbols &allow-other-keys) "No-op for unsupported $cquery/publishSemanticHighlighting extension") + +;; FIXME: A horrible hack of Flymake's insufficient API that must go +;; into Emacs master, or better, 26.2 +(when (version< emacs-version "27.0") + (cl-defstruct (eglot--diag (:include flymake--diag) + (:constructor eglot--make-diag-1)) + data-1) + (defsubst eglot--make-diag (buffer beg end type text data) + (let ((sym (alist-get type eglot--diag-error-types-to-old-types))) + (eglot--make-diag-1 :buffer buffer :beg beg :end end :type sym + :text text :data-1 data))) + (defsubst eglot--diag-data (diag) + (and (eglot--diag-p diag) (eglot--diag-data-1 diag))) + (defvar eglot--diag-error-types-to-old-types + '((eglot-error . :error) + (eglot-warning . :warning) + (eglot-note . :note))) + (advice-add + 'flymake--highlight-line :after + (lambda (diag) + (when (eglot--diag-p diag) + (let ((ov (cl-find diag + (overlays-at (flymake-diagnostic-beg diag)) + :key (lambda (ov) + (overlay-get ov 'flymake-diagnostic)))) + (overlay-properties + (get (car (rassoc (flymake-diagnostic-type diag) + eglot--diag-error-types-to-old-types)) + 'flymake-overlay-control))) + (cl-loop for (k . v) in overlay-properties + do (overlay-put ov k v))))) + '((name . eglot-hacking-in-some-per-diag-overlay-properties)))) + + (provide 'eglot) ;;; eglot.el ends here From b03cf2115b4f69903be5b653884c4d60dbf812f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 5 Jun 2018 17:28:03 +0100 Subject: [PATCH 211/771] Shoosh compiler * eglot.el (eglot-server-ready-p): Use cl-defmethod --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 66ba03a6bd3..950cf6ada89 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1153,7 +1153,7 @@ THINGS are either registrations or unregisterations." (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") -(defmethod eglot-server-ready-p (_s _what) +(cl-defmethod eglot-server-ready-p (_s _what) "Normally ready if no outstanding changes." (not eglot--recent-changes)) (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") From ae85cee296c20d7c58866fafcba8755b159d4fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 6 Jun 2018 11:09:43 +0100 Subject: [PATCH 212/771] * eglot.el (eglot--make-process): use 'utf-8-emacs-unix Attempt to improve the situation reported in https://github.com/joaotavora/eglot/issues/14. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 950cf6ada89..5ede7910356 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -258,7 +258,7 @@ CONTACT is in `eglot'. Returns a process object." (apply #'open-network-stream name stdout contact)) (t (make-process :name name :command contact :buffer stdout - :coding 'no-conversion :connection-type 'pipe + :coding 'utf-8-emacs-unix :connection-type 'pipe :stderr (setq stderr (format "*%s stderr*" name))))))) (process-put proc 'eglot-stderr stderr) (set-process-buffer proc (get-buffer-create stdout)) From 34f10965a9ec00adf56a3cba55f53ae199d388c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 6 Jun 2018 12:04:39 +0100 Subject: [PATCH 213/771] Slightly polish the flymake integration For backends, like RLS, that don't textDocument/publishDiagnostics right away, assume that the file is clean. Since Flymake allows multiple reportings, it should be OK. * eglot.el (eglot--unreported-diagnostics): Move variable up here. (eglot--maybe-activate-editing-mode): Assume no diagnostics on open. --- lisp/progmodes/eglot.el | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5ede7910356..81a63fdbea9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -900,6 +900,9 @@ If optional MARKERS, make markers." (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) +(defvar-local eglot--unreported-diagnostics nil + "Unreported Flymake diagnostics for this buffer.") + (defun eglot--maybe-activate-editing-mode (&optional server) "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In @@ -908,6 +911,7 @@ that case, also signal textDocument/didOpen." (let* ((cur (and buffer-file-name (eglot--current-server))) (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server + (setq eglot--unreported-diagnostics `(:just-opened . nil)) (eglot--managed-mode-onoff server 1) (eglot--signal-textDocument/didOpen)))) @@ -1057,9 +1061,6 @@ function with the server still running." (_server (_method (eql :telemetry/event)) &rest _any) "Handle notification telemetry/event") ;; noop, use events buffer -(defvar-local eglot--unreported-diagnostics nil - "Unreported diagnostics for this buffer.") - (cl-defmethod eglot-handle-notification (server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" From 43d9c7b8653e5fb4c5b5652b9a1e6135b0d2781d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 8 Jun 2018 16:05:02 +0100 Subject: [PATCH 214/771] Support json.c. api purely based on classes No more jsonrpc-connect. This is a big commit because of a data loss problem. It should be at least two separate commits (json.c-support and new API) * eglot.el (eglot-server-programs): Rework docstring. (eglot-handle-request): Don't take ID param (eglot-lsp-server): No more initargs. (eglot--interactive): Return 5 args. (eglot): Take 5 args. (eglot-reconnect): Pass 6 args to eglot--connect. (eglot--dispatch): Remove. (eglot--connect): Take 6 args. Rework. (eglot-handle-notification): Change all specializations to use a non-keyword symbol spec. (eglot-handle-request): Remove ID param from all specializations. Don't pass ID to jsonrpc-reply. (eglot--register-unregister): Don't take JSONRPC-ID arg. Don't pass ID to jsonrpc-reply. * jsonrpc-tests.el (returns-3, signals-an--32603-JSONRPC-error) (times-out, stretching-it-but-works) (json-el-cant-serialize-this, jsonrpc-connection-ready-p) (deferred-action-intime, deferred-action-toolate) (deferred-action-timeout): Pass JSON objects compatible with json.c (jsonrpc--test-client, jsonrpc--test-endpoint): New classes (jsonrpc--with-emacsrpc-fixture): Don't use jsonrpc-connect. (jsonrpc-connection-ready-p): Update signature. * jsonrpc.el: Rewrite commentary. (jsonrpc-connection): Rework class. (jsonrpc-process-connection): Rework class. (initialize-instance): New methods.. (jsonrpc--json-read, jsonrpc--json-encode): Reindent. (jsonrpc-connect): Delete. (jsonrpc--json-read, jsonrpc--json-encode): New functions for working with json.c (jsonrpc--process-filter): Call them. (jsonrpc--unanswered-request-id): New variable. (jsonrpc--connection-receive): Use jsonrpc--unanswered-request-id (jsonrpc-connection-send): Take keyword params to build message instead of message. (jsonrpc-notify, jsonrpc--async-request-1): Use new jsonrpc-connection-send. (jsonrpc-reply): Simplify. * eglot-tests.el (rls-watches-files, rls-basic-diagnostics) (rls-hover-after-edit): Correctly compare using string= and non-keyword symbols. --- lisp/progmodes/eglot.el | 171 ++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 77 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d5498f95334..e1592ab3b16 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -79,8 +79,8 @@ (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) "How the command `eglot' guesses the server to start. -An association list of (MAJOR-MODE . SPEC) pair. MAJOR-MODE is a -mode symbol. SPEC is +An association list of (MAJOR-MODE . CONTACT) pair. MAJOR-MODE +is a mode symbol. CONTACT is: * In the most common case, a list of strings (PROGRAM [ARGS...]). PROGRAM is called with ARGS and is expected to serve LSP requests @@ -91,12 +91,15 @@ a positive integer number for connecting to a server via TCP. Remaining ARGS are passed to `open-network-stream' for upgrading the connection with encryption or other capabilities. -* A function of no arguments returning a connected process. - -* A cons (CLASS-NAME . SPEC) where CLASS-NAME is a symbol -designating a subclass of symbol `eglot-lsp-server', for -representing experimental LSP servers. In this case SPEC is -interpreted as described above this point.") +* A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol +designating a subclass of `eglot-lsp-server', for representing +experimental LSP servers. INITARGS is a keyword-value plist used +to initialize CLASS-NAME, or a plain list interpreted as the +previous descriptions of CONTACT, in which case it is converted +to produce a plist with a suitable :PROCESS initarg to +CLASS-NAME. The class `eglot-lsp-server' descends +`jsonrpc-process-connection', which you should see for semantics +of the mandatory :PROCESS argument.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -124,8 +127,8 @@ lasted more than that many seconds." "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) `(save-excursion (save-restriction (widen) ,@body))) -(cl-defgeneric eglot-handle-request (server method id &rest params) - "Handle SERVER's METHOD request with ID and PARAMS.") +(cl-defgeneric eglot-handle-request (server method &rest params) + "Handle SERVER's METHOD request with PARAMS.") (cl-defgeneric eglot-handle-notification (server method id &rest params) "Handle SERVER's METHOD notification with PARAMS.") @@ -164,10 +167,10 @@ lasted more than that many seconds." (defclass eglot-lsp-server (jsonrpc-process-connection) ((project-nickname :documentation "Short nickname for the associated project." - :initarg :project-nickname :accessor eglot--project-nickname) + :accessor eglot--project-nickname) (major-mode :documentation "Major mode symbol." - :initarg :major-mode :accessor eglot--major-mode) + :accessor eglot--major-mode) (capabilities :documentation "JSON object containing server capabilities." :accessor eglot--capabilities) @@ -176,19 +179,22 @@ lasted more than that many seconds." :accessor eglot--shutdown-requested) (project :documentation "Project associated with server." - :initarg :project :accessor eglot--project) + :accessor eglot--project) (spinner :documentation "List (ID DOING-WHAT DONE-P) representing server progress." :initform `(nil nil t) :accessor eglot--spinner) (inhibit-autoreconnect :documentation "Generalized boolean inhibiting auto-reconnection if true." - :initarg :inhibit-autoreconnect :accessor eglot--inhibit-autoreconnect) + :accessor eglot--inhibit-autoreconnect) (file-watches :documentation "Map ID to list of WATCHES for `didChangeWatchedFiles'." :initform (make-hash-table :test #'equal) :accessor eglot--file-watches) (managed-buffers :documentation "List of buffers managed by server." - :initarg :managed-buffers :accessor eglot--managed-buffers)) + :accessor eglot--managed-buffers) + (saved-initargs + :documentation "Saved initargs for reconnection purposes" + :accessor eglot--saved-initargs)) :documentation "Represents a server. Wraps a process for LSP communication.") @@ -296,47 +302,35 @@ function with the server still running." (list (match-string 1 s) (string-to-number (match-string 2 s))) (split-string-and-unquote s))) guess))) - (list managed-mode project (cons class contact) t))) + (list managed-mode project class contact t))) ;;;###autoload -(defun eglot (managed-major-mode project contact &optional interactive) +(defun eglot (managed-major-mode project class contact &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. -The LSP server is started (or contacted) via CONTACT. If this -operation is successful, current *and future* file buffers of -MANAGED-MAJOR-MODE inside PROJECT automatically become +The LSP server of CLASS started (or contacted) via CONTACT. If +this operation is successful, current *and future* file buffers +of MANAGED-MAJOR-MODE inside PROJECT automatically become \"managed\" by the LSP server, meaning information about their contents is exchanged periodically to provide enhanced code-analysis via `xref-find-definitions', `flymake-mode', `eldoc-mode', `completion-at-point', among others. Interactively, the command attempts to guess MANAGED-MAJOR-MODE -from current buffer, CONTACT from `eglot-server-programs' and -PROJECT from `project-current'. If it can't guess, the user is -prompted. With a single \\[universal-argument] prefix arg, it -always prompt for COMMAND. With two \\[universal-argument] -prefix args, also prompts for MANAGED-MAJOR-MODE. +from current buffer, CLASS and CONTACT from +`eglot-server-programs' and PROJECT from `project-current'. If +it can't guess, the user is prompted. With a single +\\[universal-argument] prefix arg, it always prompt for COMMAND. +With two \\[universal-argument] prefix args, also prompts for +MANAGED-MAJOR-MODE. PROJECT is a project instance as returned by `project-current'. -CONTACT specifies how to contact the server. It can be: +CLASS is a subclass of symbol `eglot-lsp-server'. -* a list of strings (COMMAND [ARGS...]) specifying how -to start a server subprocess to connect to. - -* A list with a string as the first element and an integer number -as the second list is interpreted as (HOST PORT [PARAMETERS...]) -and connects to an existing server via TCP, with the remaining -PARAMETERS being given as `open-network-stream's optional -arguments. - -* A list (CLASS-SYM CONTACT...) where CLASS-SYM names the -subclass of `eglot-server' used to create the server object. The -remaining arguments are processed as described in the previous -paragraphs. - -* A function of arguments returning arguments compatible with the -previous description. +CONTACT specifies how to contact the server. It is a +keyword-value plist used to initialize CLASS or a plain list as +described in `eglot-server-programs', which see. INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) @@ -354,6 +348,7 @@ INTERACTIVE is t if called interactively." managed-major-mode (format "%s/%s" nickname managed-major-mode) nickname + class contact))) (eglot--message "Connected! Process `%s' now \ managing `%s' buffers in project `%s'." @@ -371,29 +366,51 @@ INTERACTIVE is t if called interactively." (eglot--major-mode server) (jsonrpc-name server) (eglot--project-nickname server) - (jsonrpc-contact server)) + (eieio-object-class-name server) + (eglot--saved-initargs server)) (eglot--message "Reconnected!")) (defalias 'eglot-events-buffer 'jsonrpc-events-buffer) (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--dispatch (server method id params) - "Dispatcher passed to `jsonrpc-connect'. -Calls a function on SERVER, METHOD ID and PARAMS." - (let ((method (intern (format ":%s" method)))) - (if id - (apply #'eglot-handle-request server id method params) - (apply #'eglot-handle-notification server method params) - (force-mode-line-update t)))) - -(defun eglot--connect (project managed-major-mode name nickname contact) +(defun eglot--connect (project managed-major-mode name nickname + class contact) "Connect to PROJECT, MANAGED-MAJOR-MODE, NAME. -And NICKNAME and CONTACT." - (let* ((contact (if (functionp contact) (funcall contact) contact)) +And don't forget NICKNAME and CLASS, CONTACT. This docstring +appeases checkdoc, that's all." + (let* ((readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) + (initargs + (cond ((keywordp (car contact)) contact) + ((integerp (cadr contact)) + `(:process ,(lambda () + (apply #'open-network-stream + readable-name nil + (car contact) (cadr contact) + (cddr contact))))) + ((stringp (car contact)) + `(:process ,(lambda () + (make-process + :name readable-name + :command contact + :connection-type 'pipe + :coding 'utf-8-emacs-unix + :stderr (get-buffer-create + (format "*%s stderr*" readable-name)))))))) + (spread + (lambda (fn) + (lambda (&rest args) + (apply fn (append (butlast args) (car (last args))))))) (server - (jsonrpc-connect name contact #'eglot--dispatch #'eglot--on-shutdown)) + (apply + #'make-instance class + :name name + :notification-dispatcher (funcall spread #'eglot-handle-notification) + :request-dispatcher (funcall spread #'eglot-handle-request) + :on-shutdown #'eglot--on-shutdown + initargs)) success) + (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) @@ -688,14 +705,14 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; (cl-defmethod eglot-handle-notification - (_server (_method (eql :window/showMessage)) &key type message) + (_server (_method (eql window/showMessage)) &key type message) "Handle notification window/showMessage" (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) (cl-defmethod eglot-handle-request - (server id (_method (eql :window/showMessageRequest)) &key type message actions) + (server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" (let (reply) (unwind-protect @@ -710,23 +727,23 @@ Uses THING, FACE, DEFS and PREPEND." '("OK")) nil t (plist-get (elt actions 0) :title))) (if reply - (jsonrpc-reply server id :result `(:title ,reply)) - (jsonrpc-reply server id + (jsonrpc-reply server :result `(:title ,reply)) + (jsonrpc-reply server :error `(:code -32800 :message "User cancelled")))))) (cl-defmethod eglot-handle-notification - (_server (_method (eql :window/logMessage)) &key _type _message) + (_server (_method (eql window/logMessage)) &key _type _message) "Handle notification window/logMessage") ;; noop, use events buffer (cl-defmethod eglot-handle-notification - (_server (_method (eql :telemetry/event)) &rest _any) + (_server (_method (eql telemetry/event)) &rest _any) "Handle notification telemetry/event") ;; noop, use events buffer (defvar-local eglot--unreported-diagnostics nil "Unreported diagnostics for this buffer.") (cl-defmethod eglot-handle-notification - (server (_method (eql :textDocument/publishDiagnostics)) &key uri diagnostics) + (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics) "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer @@ -750,7 +767,7 @@ Uses THING, FACE, DEFS and PREPEND." (setq eglot--unreported-diagnostics (cons t diags)))))) (jsonrpc--debug server "Diagnostics received for unvisited %s" uri))) -(cl-defun eglot--register-unregister (server jsonrpc-id things how) +(cl-defun eglot--register-unregister (server things how) "Helper for `registerCapability'. THINGS are either registrations or unregisterations." (dolist (thing (cl-coerce things 'list)) @@ -762,28 +779,28 @@ THINGS are either registrations or unregisterations." (unless (eq t (car retval)) (cl-return-from eglot--register-unregister (jsonrpc-reply - server jsonrpc-id + server :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (jsonrpc-reply server jsonrpc-id :result `(:message "OK"))) + (jsonrpc-reply server :result `(:message "OK"))) (cl-defmethod eglot-handle-request - (server id (_method (eql :client/registerCapability)) &key registrations) + (server (_method (eql client/registerCapability)) &key registrations) "Handle server request client/registerCapability" - (eglot--register-unregister server id registrations 'register)) + (eglot--register-unregister server registrations 'register)) (cl-defmethod eglot-handle-request - (server id (_method (eql :client/unregisterCapability)) + (server (_method (eql client/unregisterCapability)) &key unregisterations) ;; XXX: "unregisterations" (sic) "Handle server request client/unregisterCapability" - (eglot--register-unregister server id unregisterations 'unregister)) + (eglot--register-unregister server unregisterations 'unregister)) (cl-defmethod eglot-handle-request - (server id (_method (eql :workspace/applyEdit)) &key _label edit) + (server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" (condition-case err (progn (eglot--apply-workspace-edit edit 'confirm) - (jsonrpc-reply server id :result `(:applied ))) - (error (jsonrpc-reply server id + (jsonrpc-reply server :result `(:applied ))) + (error (jsonrpc-reply server :result `(:applied :json-false) :error `(:code -32001 :message (format "%s" ,err)))))) @@ -1348,7 +1365,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (and (equal "Indexing" what) done))))) (cl-defmethod eglot-handle-notification - ((server eglot-rls) (_method (eql :window/progress)) + ((server eglot-rls) (_method (eql window/progress)) &key id done title message &allow-other-keys) "Handle notification window/progress" (setf (eglot--spinner server) (list id title done message))) @@ -1367,17 +1384,17 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :progressReportFrequencyMs -1))) (cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql :$cquery/progress)) + ((_server eglot-cquery) (_method (eql $cquery/progress)) &rest counts &key _activeThreads &allow-other-keys) "No-op for noisy $cquery/progress extension") (cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql :$cquery/setInactiveRegions)) + ((_server eglot-cquery) (_method (eql $cquery/setInactiveRegions)) &key _uri _inactiveRegions &allow-other-keys) "No-op for unsupported $cquery/setInactiveRegions extension") (cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql :$cquery/publishSemanticHighlighting)) + ((_server eglot-cquery) (_method (eql $cquery/publishSemanticHighlighting)) &key _uri _symbols &allow-other-keys) "No-op for unsupported $cquery/publishSemanticHighlighting extension") From 8def9a619605708a1b8783093f031c054b3ad585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 8 Jun 2018 18:37:54 +0100 Subject: [PATCH 215/771] Simplify jsonrpc connection shutdown * eglot.el (eglot--process): Delete. (eglot-shutdown): Use jsonrpc-shutdown. (eglot--on-shutdown): Simplify. (eglot-reconnect): Simplify. (eglot--connect): Simplify. * jsonrpc-tests.el (jsonrpc--with-emacsrpc-fixture): Simplify. * jsonrpc.el (jsonrpc-process-type, jsonrpc-running-p) (jsonrpc-shutdown): New methods. * eglot-tests.el (auto-reconnect): Use jsonrpc--process. (eglot--call-with-dirs-and-files): Use jsonrpc-running-p. --- lisp/progmodes/eglot.el | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e1592ab3b16..cdb1c5b097c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -203,11 +203,6 @@ lasted more than that many seconds." (defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -;; HACK: Do something to fix this in the jsonrpc API or here, but in -;; the meantime concentrate the hack here. -(defalias 'eglot--process 'jsonrpc--process - "An abuse of `jsonrpc--process', a jsonrpc.el internal.") - (defun eglot-shutdown (server &optional _interactive) "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond. Don't leave this @@ -218,16 +213,15 @@ function with the server still running." (progn (setf (eglot--shutdown-requested server) t) (jsonrpc-request server :shutdown nil :timeout 3) - ;; this one is supposed to always fail, hence ignore-errors + ;; this one is supposed to always fail, because it asks the + ;; server to exit itself. Hence ignore-errors. (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) - (while (progn (accept-process-output nil 0.1) - (not (eq (eglot--shutdown-requested server) :sentinel-done))) - (eglot--warn "Sentinel for %s still hasn't run, brutally deleting it!" - (eglot--process server)) - (delete-process (eglot--process server))))) + ;; Now ask jsonrpc.el to shutdown server (which in normal + ;; conditions should return immediately). + (jsonrpc-shutdown server))) (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." @@ -243,7 +237,7 @@ function with the server still running." (delq server (gethash (eglot--project server) eglot--servers-by-project))) (cond ((eglot--shutdown-requested server) - (setf (eglot--shutdown-requested server) :sentinel-done)) + t) ((not (eglot--inhibit-autoreconnect server)) (eglot--warn "Reconnecting after unexpected server exit.") (eglot-reconnect server)) @@ -337,8 +331,7 @@ INTERACTIVE is t if called interactively." (let* ((nickname (file-name-base (directory-file-name (car (project-roots project))))) (current-server (jsonrpc-current-connection)) - (live-p (and current-server - (process-live-p (eglot--process current-server))))) + (live-p (and current-server (jsonrpc-running-p current-server)))) (if (and live-p interactive (y-or-n-p "[eglot] Live process found, reconnect instead? ")) @@ -360,7 +353,7 @@ managing `%s' buffers in project `%s'." "Reconnect to SERVER. INTERACTIVE is t if called interactively." (interactive (list (jsonrpc-current-connection-or-lose) t)) - (when (process-live-p (eglot--process server)) + (when (jsonrpc-running-p server) (ignore-errors (eglot-shutdown server interactive))) (eglot--connect (eglot--project server) (eglot--major-mode server) @@ -421,9 +414,7 @@ appeases checkdoc, that's all." (jsonrpc-request server :initialize - (list :processId (unless (eq (process-type - (eglot--process server)) - 'network) + (list :processId (unless (eq (jsonrpc-process-type server) 'network) (emacs-pid)) :rootPath (expand-file-name (car (project-roots project))) @@ -446,7 +437,7 @@ appeases checkdoc, that's all." (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) (setq success server)) - (unless (or success (not (process-live-p (eglot--process server)))) + (when (and (not success) (jsonrpc-running-p server)) (eglot-shutdown server))))) From 96edec8d36eaea04134ea9070746835b7f557bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 8 Jun 2018 18:58:34 +0100 Subject: [PATCH 216/771] Check flymake-mode before calling report-fn * eglot.el (eglot-handle-notification): Check flymake-mode. GitHub-reference: close https://github.com/joaotavora/eglot/issues/16 --- lisp/progmodes/eglot.el | 94 ++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 81a63fdbea9..97db8fc3f7c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -696,13 +696,13 @@ happens, the original timer keeps counting). Return (ID TIMER)." (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) - (eglot--debug - server `(:message "success ignored" :id ,id)))) + (eglot--debug + server `(:message "success ignored" :id ,id)))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status server) `(,message t)) - server `(:message "error ignored, status set" - :id ,id :error ,code))) + (setf (eglot--status server) `(,message t)) + server `(:message "error ignored, status set" + :id ,id :error ,code))) (or timer (funcall make-timer))) (eglot--pending-continuations server)) (list id timer))) @@ -726,8 +726,8 @@ DEFERRED is passed to `eglot--async-request', which see." :success-fn (lambda (result) (throw done `(done ,result))) :timeout-fn (lambda () (throw done '(error "Timed out"))) :error-fn (eglot--lambda (&key code message _data) - (throw done `(error - ,(format "Ooops: %s: %s" code message)))) + (throw done `(error + ,(format "Ooops: %s: %s" code message)))) :deferred deferred)) (while t (accept-process-output nil 30))) (pcase-let ((`(,id ,timer) id-and-timer)) @@ -1079,7 +1079,7 @@ function with the server still running." (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) into diags - finally (cond (eglot--current-flymake-report-fn + finally (cond ((and flymake-mode eglot--current-flymake-report-fn) (funcall eglot--current-flymake-report-fn diags) (setq eglot--unreported-diagnostics nil)) (t @@ -1286,15 +1286,15 @@ DUMMY is ignored." (setq eglot--xref-known-symbols (mapcar (eglot--lambda (&key name kind location containerName) - (propertize name - :textDocumentPositionParams - (list :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) - :locations (list location) - :kind kind - :containerName containerName)) + (propertize name + :textDocumentPositionParams + (list :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) + :locations (list location) + :kind kind + :containerName containerName)) (eglot--request server :textDocument/documentSymbol `(:textDocument ,text-id)))) (all-completions string eglot--xref-known-symbols)))))) @@ -1316,7 +1316,7 @@ DUMMY is ignored." (get-text-property 0 :textDocumentPositionParams identifier))))) (mapcar (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri (plist-get range :start))) location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) @@ -1329,7 +1329,7 @@ DUMMY is ignored." (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) (mapcar (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri (plist-get range :start))) (eglot--request (eglot--current-server-or-lose) :textDocument/references (append @@ -1339,8 +1339,8 @@ DUMMY is ignored." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) (mapcar (eglot--lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) + (cl-destructuring-bind (&key uri range) location + (eglot--xref-make name uri (plist-get range :start)))) (eglot--request (eglot--current-server-or-lose) :workspace/symbol (list :query pattern))))) @@ -1362,10 +1362,10 @@ DUMMY is ignored." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (eglot--lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) @@ -1459,20 +1459,20 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." server :textDocument/signatureHelp position-params :success-fn (eglot--lambda (&key signatures activeSignature activeParameter) - (when-buffer-window - (when (cl-plusp (length signatures)) - (setq sig-showing t) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter))))) + (when-buffer-window + (when (cl-plusp (length signatures)) + (setq sig-showing t) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter))))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (eglot--async-request server :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) - (unless sig-showing - (when-buffer-window - (eldoc-message (eglot--hover-info contents range))))) + (unless sig-showing + (when-buffer-window + (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request @@ -1482,12 +1482,12 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (setq eglot--highlights (when-buffer-window (mapcar (eglot--lambda (&key range _kind _role) - (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))) + (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) @@ -1498,9 +1498,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let ((entries (mapcar (eglot--lambda (&key name kind location _containerName) - (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) - (eglot--lsp-position-to-point - (plist-get (plist-get location :range) :start)))) + (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) + (eglot--lsp-position-to-point + (plist-get (plist-get location :range) :start)))) (eglot--request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) @@ -1519,7 +1519,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc (pcase-lambda (`(,newText ,beg . ,end)) (goto-char beg) (delete-region beg end) (insert newText)) (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) + (cons newText (eglot--range-region range 'markers))) edits))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) @@ -1528,8 +1528,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cl-destructuring-bind (&key changes documentChanges) wedit (let ((prepared (mapcar (eglot--lambda (&key textDocument edits) - (cl-destructuring-bind (&key uri version) textDocument - (list (eglot--uri-to-path uri) edits version))) + (cl-destructuring-bind (&key uri version) textDocument + (list (eglot--uri-to-path uri) edits version))) documentChanges))) (cl-loop for (uri edits) on changes by #'cddr do (push (list (eglot--uri-to-path uri) edits) prepared)) @@ -1589,7 +1589,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) (menu-items (mapcar (eglot--lambda (&key title command arguments) - `(,title . (:command ,command :arguments ,arguments))) + `(,title . (:command ,command :arguments ,arguments))) actions)) (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) (command-and-args From af32ce29ef3f9f5d93ad4ce57b9cf1a59435455e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 9 Jun 2018 21:09:03 +0100 Subject: [PATCH 217/771] Attempt to handle rls sophisticated globs for didchangewwatchedfiles * eglot.el (eglot--wildcard-to-regexp): New helper. (eglot--register-workspace/didChangeWatchedFiles): Use it. --- lisp/progmodes/eglot.el | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 97db8fc3f7c..9dd98f0b9bb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1610,6 +1610,18 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ;;; Dynamic registration ;;; +(defun eglot--wildcard-to-regexp (wildcard) + "(Very lame attempt to) convert WILDCARD to a Elisp regexp." + (cl-loop + with substs = '(("{" . "\\\\(") + ("}" . "\\\\)") + ("," . "\\\\|")) + with string = (wildcard-to-regexp wildcard) + for (pattern . rep) in substs + for target = string then result + for result = (replace-regexp-in-string pattern rep target) + finally return result)) + (cl-defun eglot--register-workspace/didChangeWatchedFiles (server &key id watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" (eglot--unregister-workspace/didChangeWatchedFiles server :id id) @@ -1623,7 +1635,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ((and (memq action '(created changed deleted)) (cl-find file globs :test (lambda (f glob) - (string-match (wildcard-to-regexp + (string-match (eglot--wildcard-to-regexp (expand-file-name glob)) f)))) (eglot--notify From 4d076195840812070153e2d23fe2dba9763a4913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 9 Jun 2018 21:12:48 +0100 Subject: [PATCH 218/771] Fix indentation f@#$%^ by previous commit Courtesy of aggressive-indent-mode... Agressive it is... --- lisp/progmodes/eglot.el | 92 ++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9dd98f0b9bb..4d3a46970e3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -696,13 +696,13 @@ happens, the original timer keeps counting). Return (ID TIMER)." (puthash id (list (or success-fn (eglot--lambda (&rest _ignored) - (eglot--debug - server `(:message "success ignored" :id ,id)))) + (eglot--debug + server `(:message "success ignored" :id ,id)))) (or error-fn (eglot--lambda (&key code message &allow-other-keys) - (setf (eglot--status server) `(,message t)) - server `(:message "error ignored, status set" - :id ,id :error ,code))) + (setf (eglot--status server) `(,message t)) + server `(:message "error ignored, status set" + :id ,id :error ,code))) (or timer (funcall make-timer))) (eglot--pending-continuations server)) (list id timer))) @@ -726,8 +726,8 @@ DEFERRED is passed to `eglot--async-request', which see." :success-fn (lambda (result) (throw done `(done ,result))) :timeout-fn (lambda () (throw done '(error "Timed out"))) :error-fn (eglot--lambda (&key code message _data) - (throw done `(error - ,(format "Ooops: %s: %s" code message)))) + (throw done `(error + ,(format "Ooops: %s: %s" code message)))) :deferred deferred)) (while t (accept-process-output nil 30))) (pcase-let ((`(,id ,timer) id-and-timer)) @@ -1286,15 +1286,15 @@ DUMMY is ignored." (setq eglot--xref-known-symbols (mapcar (eglot--lambda (&key name kind location containerName) - (propertize name - :textDocumentPositionParams - (list :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) - :locations (list location) - :kind kind - :containerName containerName)) + (propertize name + :textDocumentPositionParams + (list :textDocument text-id + :position (plist-get + (plist-get location :range) + :start)) + :locations (list location) + :kind kind + :containerName containerName)) (eglot--request server :textDocument/documentSymbol `(:textDocument ,text-id)))) (all-completions string eglot--xref-known-symbols)))))) @@ -1316,7 +1316,7 @@ DUMMY is ignored." (get-text-property 0 :textDocumentPositionParams identifier))))) (mapcar (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri (plist-get range :start))) location-or-locations))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) @@ -1329,7 +1329,7 @@ DUMMY is ignored." (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) (mapcar (eglot--lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri (plist-get range :start))) (eglot--request (eglot--current-server-or-lose) :textDocument/references (append @@ -1339,8 +1339,8 @@ DUMMY is ignored." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) (mapcar (eglot--lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) + (cl-destructuring-bind (&key uri range) location + (eglot--xref-make name uri (plist-get range :start)))) (eglot--request (eglot--current-server-or-lose) :workspace/symbol (list :query pattern))))) @@ -1362,10 +1362,10 @@ DUMMY is ignored." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (eglot--lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) @@ -1459,20 +1459,20 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." server :textDocument/signatureHelp position-params :success-fn (eglot--lambda (&key signatures activeSignature activeParameter) - (when-buffer-window - (when (cl-plusp (length signatures)) - (setq sig-showing t) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter))))) + (when-buffer-window + (when (cl-plusp (length signatures)) + (setq sig-showing t) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter))))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (eglot--async-request server :textDocument/hover position-params :success-fn (eglot--lambda (&key contents range) - (unless sig-showing - (when-buffer-window - (eldoc-message (eglot--hover-info contents range))))) + (unless sig-showing + (when-buffer-window + (eldoc-message (eglot--hover-info contents range))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--async-request @@ -1482,12 +1482,12 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (setq eglot--highlights (when-buffer-window (mapcar (eglot--lambda (&key range _kind _role) - (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))) + (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) @@ -1498,9 +1498,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let ((entries (mapcar (eglot--lambda (&key name kind location _containerName) - (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) - (eglot--lsp-position-to-point - (plist-get (plist-get location :range) :start)))) + (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) + (eglot--lsp-position-to-point + (plist-get (plist-get location :range) :start)))) (eglot--request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) @@ -1519,7 +1519,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapc (pcase-lambda (`(,newText ,beg . ,end)) (goto-char beg) (delete-region beg end) (insert newText)) (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) + (cons newText (eglot--range-region range 'markers))) edits))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) @@ -1528,8 +1528,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cl-destructuring-bind (&key changes documentChanges) wedit (let ((prepared (mapcar (eglot--lambda (&key textDocument edits) - (cl-destructuring-bind (&key uri version) textDocument - (list (eglot--uri-to-path uri) edits version))) + (cl-destructuring-bind (&key uri version) textDocument + (list (eglot--uri-to-path uri) edits version))) documentChanges))) (cl-loop for (uri edits) on changes by #'cddr do (push (list (eglot--uri-to-path uri) edits) prepared)) @@ -1589,7 +1589,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) (menu-items (mapcar (eglot--lambda (&key title command arguments) - `(,title . (:command ,command :arguments ,arguments))) + `(,title . (:command ,command :arguments ,arguments))) actions)) (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) (command-and-args From ce96614d8acd0c12b8d209678bbcc2af5b95d1c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 9 Jun 2018 21:19:13 +0100 Subject: [PATCH 219/771] Request dispatcher's return value determines response No more jsonrpc-reply. * eglot.el (eglot-handle-request window/showMessageRequest): Simplify. (eglot--register-unregister): Simplify. (eglot-handle-request workspace/applyEdit): Simplify. (eglot--apply-text-edits): Signal a jsonrpc-error. (eglot--apply-workspace-edit): Simplify. * jsonrpc-tests.el (jsonrpc--with-emacsrpc-fixture): Don't jsonrpc--reply. * jsonrpc.el (jsonrpc-error, jsonrpc-connection, jsonrpc-request): Improve docstring. (jsonrpc-error): Polymorphic args. (jsonrpc--unanswered-request-id): Remove. (jsonrpc--connection-receive): Rework and simplify. (jsonrpc-reply): Simplify. --- lisp/progmodes/eglot.el | 87 ++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 60a03228dd1..13413ab07fc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -707,24 +707,18 @@ Uses THING, FACE, DEFS and PREPEND." type message)) (cl-defmethod eglot-handle-request - (server (_method (eql window/showMessageRequest)) &key type message actions) + (_server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" - (let (reply) - (unwind-protect - (setq reply - (completing-read - (concat - (format (propertize "[eglot] Server reports (type=%s): %s" - 'face (if (<= type 1) 'error)) - type message) - "\nChoose an option: ") - (or (mapcar (lambda (obj) (plist-get obj :title)) actions) - '("OK")) - nil t (plist-get (elt actions 0) :title))) - (if reply - (jsonrpc-reply server :result `(:title ,reply)) - (jsonrpc-reply server - :error `(:code -32800 :message "User cancelled")))))) + (or (completing-read + (concat + (format (propertize "[eglot] Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message) + "\nChoose an option: ") + (or (mapcar (lambda (obj) (plist-get obj :title)) actions) + '("OK")) + nil t (plist-get (elt actions 0) :title)) + (jsonrpc-error :code -32800 :message "User cancelled"))) (cl-defmethod eglot-handle-notification (_server (_method (eql window/logMessage)) &key _type _message) @@ -762,18 +756,13 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defun eglot--register-unregister (server things how) "Helper for `registerCapability'. THINGS are either registrations or unregisterations." - (dolist (thing (cl-coerce things 'list)) - (cl-destructuring-bind (&key id method registerOptions) thing - (let (retval) - (unwind-protect - (setq retval (apply (intern (format "eglot--%s-%s" how method)) - server :id id registerOptions)) - (unless (eq t (car retval)) - (cl-return-from eglot--register-unregister - (jsonrpc-reply - server - :error `(:code -32601 :message ,(or (cadr retval) "sorry"))))))))) - (jsonrpc-reply server :result `(:message "OK"))) + (cl-loop + for thing in (cl-coerce things 'list) + collect (cl-destructuring-bind (&key id method registerOptions) thing + (apply (intern (format "eglot--%s-%s" how method)) + server :id id registerOptions)) + into results + finally return `(:ok ,@results))) (cl-defmethod eglot-handle-request (server (_method (eql client/registerCapability)) &key registrations) @@ -787,14 +776,9 @@ THINGS are either registrations or unregisterations." (eglot--register-unregister server unregisterations 'unregister)) (cl-defmethod eglot-handle-request - (server (_method (eql workspace/applyEdit)) &key _label edit) + (_server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" - (condition-case err - (progn (eglot--apply-workspace-edit edit 'confirm) - (jsonrpc-reply server :result `(:applied ))) - (error (jsonrpc-reply server - :result `(:applied :json-false) - :error `(:code -32001 :message (format "%s" ,err)))))) + (eglot--apply-workspace-edit edit 'confirm)) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." @@ -1206,8 +1190,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." (unless (or (not version) (equal version eglot--versioned-identifier)) - (eglot--error "Edits on `%s' require version %d, you have %d" - (current-buffer) version eglot--versioned-identifier)) + (jsonrpc-error "Edits on `%s' require version %d, we have %d" + (current-buffer) version eglot--versioned-identifier)) (eglot--widening (mapc (pcase-lambda (`(,newText ,beg . ,end)) (goto-char beg) (delete-region beg end) (insert newText)) @@ -1223,7 +1207,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapcar (jsonrpc-lambda (&key textDocument edits) (cl-destructuring-bind (&key uri version) textDocument (list (eglot--uri-to-path uri) edits version))) - documentChanges))) + documentChanges)) + edit) (cl-loop for (uri edits) on changes by #'cddr do (push (list (eglot--uri-to-path uri) edits) prepared)) (if (or confirm @@ -1233,16 +1218,17 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (format "[eglot] Server wants to edit:\n %s\n Proceed? " (mapconcat #'identity (mapcar #'car prepared) "\n "))) (eglot--error "User cancelled server edit"))) + (while (setq edit (car prepared)) + (cl-destructuring-bind (path edits &optional version) edit + (with-current-buffer (find-file-noselect path) + (eglot--apply-text-edits edits version)) + (pop prepared)) + t) (unwind-protect - (let (edit) (while (setq edit (car prepared)) - (cl-destructuring-bind (path edits &optional version) edit - (with-current-buffer (find-file-noselect path) - (eglot--apply-text-edits edits version)) - (pop prepared)))) - (if prepared (eglot--warn "Caution: edits of files %s failed." - (mapcar #'car prepared)) - (eglot-eldoc-function) - (eglot--message "Edit successful!")))))) + (if prepared (eglot--warn "Caution: edits of files %s failed." + (mapcar #'car prepared)) + (eglot-eldoc-function) + (eglot--message "Edit successful!")))))) (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." @@ -1345,7 +1331,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) (push (file-notify-add-watch dir '(change) #'handle-event) (gethash id (eglot--file-watches server)))) - (setq success `(t "OK"))) + (setq + success + `(:message ,(format "OK, watching %s watchers" + (length watchers))))) (unless success (eglot--unregister-workspace/didChangeWatchedFiles server :id id)))))) From 4d680281947ad05399c9b3fc9274ef33c07d3855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 10 Jun 2018 07:16:41 +0100 Subject: [PATCH 220/771] Simplify jsonrpc status setting * eglot.el (eglot--connect): Don't set jsonrpc-status. (eglot-clear-status): New interactive command. (eglot--mode-line-format): Simplify. * jsonrpc.el (jsonrpc--async-request-1): Simplify. (jsonrpc-connection): Replace status with last-error. (jsonrpc-clear-status): Delete. (jsonrpc--connection-receive): Set last-error. --- lisp/progmodes/eglot.el | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13413ab07fc..f8bd32cd97a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -423,7 +423,6 @@ appeases checkdoc, that's all." :initializationOptions (eglot-initialization-options server) :capabilities (eglot-client-capabilities server))) (setf (eglot--capabilities server) capabilities) - (setf (jsonrpc-status server) nil) (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) @@ -613,6 +612,11 @@ that case, also signal textDocument/didOpen." (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) +(defun eglot-clear-status (server) + "Clear the last JSONRPC error for SERVER." + (interactive (list (jsonrpc-current-connection-or-lose))) + (setf (jsonrpc-last-error server) nil)) + ;;; Mode-line, menu and other sugar ;;; @@ -652,7 +656,7 @@ Uses THING, FACE, DEFS and PREPEND." (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) (`(,_id ,doing ,done-p ,detail) (and server (eglot--spinner server))) - (`(,status ,serious-p) (and server (jsonrpc-status server)))) + (last-error (and server (jsonrpc-last-error server)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) (when nick @@ -662,11 +666,12 @@ Uses THING, FACE, DEFS and PREPEND." (mouse-1 eglot-events-buffer "go to events buffer") (mouse-2 eglot-shutdown "quit server") (mouse-3 eglot-reconnect "reconnect to server"))) - ,@(when serious-p + ,@(when last-error `("/" ,(eglot--mode-line-props "error" 'compilation-mode-line-fail - '((mouse-3 jsonrpc-clear-status "clear this status")) - (format "An error occured: %s\n" status)))) + '((mouse-3 eglot-clear-status "clear this status")) + (format "An error occured: %s\n" (plist-get last-error + :message))))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props (format "%s%s" doing From bbfc1fdcf605b010960023e073ff0bf6da3886e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 10 Jun 2018 07:31:45 +0100 Subject: [PATCH 221/771] Remove connection grabbing antics from jsonrpc.el * eglot.el (eglot--managed-mode): Don't touch jsonrpc-find-connection-functions. (eglot--current-server-or-lose, eglot--current-server): New functions (resuscitate). (eglot-shutdown, eglot, eglot-reconnect) (eglot--server-capable, eglot--maybe-activate-editing-mode) (eglot-clear-status, eglot--mode-line-format) (eglot--signal-textDocument/didChange) (eglot--signal-textDocument/didOpen) (eglot--signal-textDocument/didSave) (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-references) (xref-backend-apropos, eglot-completion-at-point) (eglot-help-at-point, eglot-eldoc-function, eglot-imenu) (eglot-rename, eglot-code-actions): Use eglot--current-server and eglot--current-server-or-lose. (eglot-events-buffer, eglot-stderr-buffer) (eglot-forget-pending-continuations): New commands. (eglot--mode-line-format): Use eglot-stderr-buffer. * jsonrpc.el (jsonrpc-find-connection-functions) (jsonrpc-current-connection, jsonrpc-current-connection-or-lose): Remove. (jsonrpc-stderr-buffer, jsonrpc-events-buffer): Simplify. (jsonrpc-forget-pending-continuations): No longer interactive. * eglot-tests.el (auto-detect-running-server, auto-reconnect): Use eglot--current-server. --- lisp/progmodes/eglot.el | 72 +++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f8bd32cd97a..32879ec320c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -207,7 +207,7 @@ lasted more than that many seconds." "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond. Don't leave this function with the server still running." - (interactive (list (jsonrpc-current-connection-or-lose) t)) + (interactive (list (eglot--current-server-or-lose) t)) (eglot--message "Asking %s politely to terminate" (jsonrpc-name server)) (unwind-protect (progn @@ -330,7 +330,7 @@ INTERACTIVE is t if called interactively." (interactive (eglot--interactive)) (let* ((nickname (file-name-base (directory-file-name (car (project-roots project))))) - (current-server (jsonrpc-current-connection)) + (current-server (eglot--current-server)) (live-p (and current-server (jsonrpc-running-p current-server)))) (if (and live-p interactive @@ -352,7 +352,7 @@ managing `%s' buffers in project `%s'." (defun eglot-reconnect (server &optional interactive) "Reconnect to SERVER. INTERACTIVE is t if called interactively." - (interactive (list (jsonrpc-current-connection-or-lose) t)) + (interactive (list (eglot--current-server-or-lose) t)) (when (jsonrpc-running-p server) (ignore-errors (eglot-shutdown server interactive))) (eglot--connect (eglot--project server) @@ -363,7 +363,20 @@ INTERACTIVE is t if called interactively." (eglot--saved-initargs server)) (eglot--message "Reconnected!")) -(defalias 'eglot-events-buffer 'jsonrpc-events-buffer) +(defun eglot-events-buffer (server) + "Display events buffer for SERVER." + (interactive (eglot--current-server-or-lose)) + (display-buffer (jsonrpc-events-buffer server))) + +(defun eglot-stderr-buffer (server) + "Display stderr buffer for SERVER." + (interactive (eglot--current-server-or-lose)) + (display-buffer (jsonrpc-stderr-buffer server))) + +(defun eglot-forget-pending-continuations (server) + "Forget pending requests for SERVER." + (interactive (eglot--current-server-or-lose)) + (jsonrpc-forget-pending-continuations server)) (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") @@ -515,7 +528,7 @@ under cursor." (unless (cl-some (lambda (feat) (memq feat eglot-ignored-server-capabilites)) feats) - (cl-loop for caps = (eglot--capabilities (jsonrpc-current-connection-or-lose)) + (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) then (cadr probe) for feat in feats for probe = (plist-member caps feat) @@ -548,7 +561,6 @@ If optional MARKERS, make markers." nil nil eglot-mode-map (cond (eglot--managed-mode - (add-hook 'jsonrpc-find-connection-functions 'eglot--find-current-server nil t) (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) @@ -562,7 +574,6 @@ If optional MARKERS, make markers." #'eglot-eldoc-function) (add-function :around (local imenu-create-index-function) #'eglot-imenu)) (t - (remove-hook 'jsonrpc-find-connection-functions 'eglot--find-current-server t) (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -589,12 +600,17 @@ If optional MARKERS, make markers." (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) -(defun eglot--find-current-server () +(defun eglot--current-server () "Find the current logical EGLOT server." (let* ((probe (or (project-current) `(transient . ,default-directory)))) (cl-find major-mode (gethash probe eglot--servers-by-project) :key #'eglot--major-mode))) +(defun eglot--current-server-or-lose () + "Return current logical EGLOT server connection or error." + (or (eglot--current-server) + (jsonrpc-error "No current JSON-RPC connection"))) + (defvar-local eglot--unreported-diagnostics nil "Unreported Flymake diagnostics for this buffer.") @@ -603,7 +619,7 @@ If optional MARKERS, make markers." If SERVER is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." ;; Called even when revert-buffer-in-progress-p - (let* ((cur (and buffer-file-name (eglot--find-current-server))) + (let* ((cur (and buffer-file-name (eglot--current-server))) (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server (setq eglot--unreported-diagnostics `(:just-opened . nil)) @@ -614,7 +630,7 @@ that case, also signal textDocument/didOpen." (defun eglot-clear-status (server) "Clear the last JSONRPC error for SERVER." - (interactive (list (jsonrpc-current-connection-or-lose))) + (interactive (list (eglot--current-server-or-lose))) (setf (jsonrpc-last-error server) nil)) @@ -651,7 +667,7 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((server (jsonrpc-current-connection)) + (pcase-let* ((server (eglot--current-server)) (nick (and server (eglot--project-nickname server))) (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) @@ -662,7 +678,7 @@ Uses THING, FACE, DEFS and PREPEND." (when nick `(":" ,(eglot--mode-line-props nick 'eglot-mode-line - '((C-mouse-1 jsonrpc-stderr-buffer "go to stderr buffer") + '((C-mouse-1 eglot-stderr-buffer "go to stderr buffer") (mouse-1 eglot-events-buffer "go to events buffer") (mouse-2 eglot-shutdown "quit server") (mouse-3 eglot-reconnect "reconnect to server"))) @@ -680,7 +696,7 @@ Uses THING, FACE, DEFS and PREPEND." ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props (format "%d oustanding requests" pending) 'warning - '((mouse-3 jsonrpc-forget-pending-continuations + '((mouse-3 eglot-forget-pending-continuations "fahgettaboudit")))))))))) (add-to-list 'mode-line-misc-info @@ -863,7 +879,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when eglot--recent-changes - (let* ((server (jsonrpc-current-connection-or-lose)) + (let* ((server (eglot--current-server-or-lose)) (sync-kind (eglot--server-capable :textDocumentSync)) (full-sync-p (or (eq sync-kind 1) (eq :emacs-messup eglot--recent-changes)))) @@ -887,18 +903,18 @@ Records START, END and PRE-CHANGE-LENGTH locally." "Send textDocument/didOpen to server." (setq eglot--recent-changes nil eglot--versioned-identifier 0) (jsonrpc-notify - (jsonrpc-current-connection-or-lose) + (eglot--current-server-or-lose) :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." (jsonrpc-notify - (jsonrpc-current-connection-or-lose) + (eglot--current-server-or-lose) :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." - (let ((server (jsonrpc-current-connection-or-lose)) + (let ((server (eglot--current-server-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) (jsonrpc-notify server :textDocument/willSave params) (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) @@ -910,7 +926,7 @@ Records START, END and PRE-CHANGE-LENGTH locally." (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." (jsonrpc-notify - (jsonrpc-current-connection-or-lose) + (eglot--current-server-or-lose) :textDocument/didSave (list ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. @@ -950,7 +966,7 @@ DUMMY is ignored." (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) - (let ((server (jsonrpc-current-connection-or-lose)) + (let ((server (eglot--current-server-or-lose)) (text-id (eglot--TextDocumentIdentifier))) (completion-table-with-cache (lambda (string) @@ -984,7 +1000,7 @@ DUMMY is ignored." (location-or-locations (if rich-identifier (get-text-property 0 :locations rich-identifier) - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) @@ -1004,7 +1020,7 @@ DUMMY is ignored." (mapcar (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/references (append params @@ -1017,14 +1033,14 @@ DUMMY is ignored." (jsonrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location (eglot--xref-make name uri (plist-get range :start)))) - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol `(:query ,pattern))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (server (jsonrpc-current-connection-or-lose))) + (server (eglot--current-server-or-lose))) (when (eglot--server-capable :completionProvider) (list (or (car bounds) (point)) @@ -1113,7 +1129,7 @@ DUMMY is ignored." "Request \"hover\" information for the thing at point." (interactive) (cl-destructuring-bind (&key contents range) - (jsonrpc-request (jsonrpc-current-connection-or-lose) :textDocument/hover + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) (let ((blurb (eglot--hover-info contents range))) @@ -1124,7 +1140,7 @@ DUMMY is ignored." "EGLOT's `eldoc-documentation-function' function. If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((buffer (current-buffer)) - (server (jsonrpc-current-connection-or-lose)) + (server (eglot--current-server-or-lose)) (position-params (eglot--TextDocumentPositionParams)) sig-showing) (cl-macrolet ((when-buffer-window @@ -1183,7 +1199,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) (append @@ -1242,7 +1258,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit - (jsonrpc-request (jsonrpc-current-connection-or-lose) + (jsonrpc-request (eglot--current-server-or-lose) :textDocument/rename `(,@(eglot--TextDocumentPositionParams) :newName ,newname)) current-prefix-arg)) @@ -1259,7 +1275,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (t (list (point-min) (point-max)))))) (unless (eglot--server-capable :codeActionProvider) (eglot--error "Server can't execute code actions!")) - (let* ((server (jsonrpc-current-connection-or-lose)) + (let* ((server (eglot--current-server-or-lose)) (actions (jsonrpc-request server :textDocument/codeAction From 1d61ff404356f024c0911623f2f3640324caa527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 10 Jun 2018 13:41:10 +0100 Subject: [PATCH 222/771] New eglot-ensure to put in a major-mode's hook * eglot.el (Commentary): Mention eglo-ensure. (eglot--connect): Rearrange args. (eglot--guess-contact): Rename from eglot--interactive. (eglot): Use eglot--guess-contact. (eglot, eglot-reconnect): Rearrange call to eglot--connect. (eglot-ensure): New command to put in mode hook. * eglot-tests.el (eglot--tests-connect): New helper. (auto-detect-running-server, auto-reconnect, rls-watches-files) (rls-basic-diagnostics, rls-hover-after-edit, rls-rename) (basic-completions, hover-after-completions): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/17 --- lisp/progmodes/eglot.el | 82 +++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4d3a46970e3..47352f75234 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -46,6 +46,13 @@ ;; To "unmanage" these buffers, shutdown the server with M-x ;; eglot-shutdown. ;; +;; You can also do: +;; +;; (add-hook 'foo-mode-hook 'eglot-ensure) +;; +;; To attempt to start an eglot session automatically everytime a +;; foo-mode buffer is visited. +;; ;;; Code: (require 'json) @@ -279,7 +286,7 @@ CONTACT is in `eglot'. Returns a process object." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") -(defun eglot--connect (project managed-major-mode contact server-class) +(defun eglot--connect (managed-major-mode project server-class contact) "Connect for PROJECT, MANAGED-MAJOR-MODE and CONTACT. INTERACTIVE is t if inside interactive call. Return an object of class SERVER-CLASS." @@ -331,8 +338,10 @@ class SERVER-CLASS." (defvar eglot--command-history nil "History of COMMAND arguments to `eglot'.") -(defun eglot--interactive () - "Helper for `eglot'." +(defun eglot--guess-contact (&optional interactive) + "Helper for `eglot'. +Return (MANAGED-MODE PROJECT CONTACT CLASS). +If INTERACTIVE, maybe prompt user." (let* ((guessed-mode (if buffer-file-name major-mode)) (managed-mode (cond @@ -349,17 +358,20 @@ class SERVER-CLASS." (class (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess))))) (program (and (listp guess) (stringp (car guess)) (car guess))) - (base-prompt "[eglot] Enter program to execute (or :): ") + (base-prompt + (and interactive + "[eglot] Enter program to execute (or :): ")) (prompt - (cond (current-prefix-arg base-prompt) - ((null guess) - (format "[eglot] Sorry, couldn't guess for `%s'\n%s!" - managed-mode base-prompt)) - ((and program (not (executable-find program))) - (concat (format "[eglot] I guess you want to run `%s'" - (combine-and-quote-strings guess)) - (format ", but I can't find `%s' in PATH!" program) - "\n" base-prompt)))) + (and base-prompt + (cond (current-prefix-arg base-prompt) + ((null guess) + (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" + managed-mode base-prompt)) + ((and program (not (executable-find program))) + (concat (format "[eglot] I guess you want to run `%s'" + (combine-and-quote-strings guess)) + (format ", but I can't find `%s' in PATH!" program) + "\n" base-prompt))))) (contact (if prompt (let ((s (read-shell-command @@ -371,10 +383,10 @@ class SERVER-CLASS." (list (match-string 1 s) (string-to-number (match-string 2 s))) (split-string-and-unquote s))) guess))) - (list managed-mode project contact class t))) + (list managed-mode project class contact))) ;;;###autoload -(defun eglot (managed-major-mode project command server-class +(defun eglot (managed-major-mode project server-class command &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. @@ -405,7 +417,7 @@ SERVER-CLASS is a symbol naming a class that must inherit from `eglot-server', or nil to use the default server class. INTERACTIVE is t if called interactively." - (interactive (eglot--interactive)) + (interactive (append (eglot--guess-contact t) '(t))) (let ((current-server (eglot--current-server))) (if (and current-server (process-live-p (eglot--process current-server)) @@ -415,10 +427,10 @@ INTERACTIVE is t if called interactively." (when (and current-server (process-live-p (eglot--process current-server))) (ignore-errors (eglot-shutdown current-server))) - (let ((server (eglot--connect project - managed-major-mode - command - server-class))) + (let ((server (eglot--connect managed-major-mode + project + server-class + command))) (eglot--message "Connected! Server `%s' now \ managing `%s' buffers in project `%s'." (eglot--name server) managed-major-mode @@ -431,12 +443,34 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose) t)) (when (process-live-p (eglot--process server)) (ignore-errors (eglot-shutdown server interactive))) - (eglot--connect (eglot--project server) - (eglot--major-mode server) - (eglot--contact server) - (eieio-object-class server)) + (eglot--connect (eglot--major-mode server) + (eglot--project server) + (eieio-object-class server) + (eglot--contact server)) (eglot--message "Reconnected!")) +(defvar eglot--managed-mode) ;forward decl + +(defun eglot-ensure () + "Start Eglot session for current buffer if there isn't one." + (let ((buffer (current-buffer))) + (cl-labels + ((maybe-connect + () + (remove-hook 'post-command-hook #'maybe-connect nil) + (eglot--with-live-buffer buffer + (if eglot--managed-mode + (eglot--message "%s is already managed by existing `%s'" + buffer + (eglot--name (eglot--current-server))) + (let ((server (apply #'eglot--connect (eglot--guess-contact)))) + (eglot--message + "Automatically started `%s' to manage `%s' buffers in project `%s'" + (eglot--name server) + major-mode + (eglot--project-nickname server))))))) + (add-hook 'post-command-hook #'maybe-connect 'append nil)))) + (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." (let ((server (process-get proc 'eglot-server))) From 7bdc94f79c7b92611ad0fd1dfb948b65b299a0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 10 Jun 2018 14:57:02 +0100 Subject: [PATCH 223/771] * eglot.el (version): bump to 0.9 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 47352f75234..59475eb7fc7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.8 +;; Version: 0.9 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From c86410efad4e638a2d9571d1ebd86bd83c1b8e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 13 Jun 2018 12:30:20 +0100 Subject: [PATCH 224/771] Fix bug in querying server capabilities This lead to javascript-typescript-stdio being sent an incremental didChange notif, which it doesn't support. * eglot.el (eglot--server-capable): Fix bug. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 59475eb7fc7..d9fbd331c1d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -867,8 +867,8 @@ under cursor." for feat in feats for probe = (plist-member caps feat) if (not probe) do (cl-return nil) - if (eq (cadr probe) t) do (cl-return t) if (eq (cadr probe) :json-false) do (cl-return nil) + if (not (listp (cadr probe))) do (cl-return (cadr probe)) finally (cl-return (or probe t))))) (defun eglot--range-region (range &optional markers) From 332657f444a14f5b7a7a1ad85c79e3fe27540723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 13 Jun 2018 12:46:36 +0100 Subject: [PATCH 225/771] * eglot.el (eglot-shutdown): accept timeout param. --- lisp/progmodes/eglot.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d9fbd331c1d..e438c492dd9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1039,14 +1039,14 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; -(defun eglot-shutdown (server &optional _interactive) +(defun eglot-shutdown (server &optional _interactive timeout) "Politely ask SERVER to quit. -Forcefully quit it if it doesn't respond. Don't leave this -function with the server still running." +Forcefully quit it if it doesn't respond within TIMEOUT seconds. +Don't leave this function with the server still running." (interactive (list (eglot--current-server-or-lose) t)) (eglot--message "Asking %s politely to terminate" (eglot--name server)) (unwind-protect - (let ((eglot-request-timeout 3)) + (let ((eglot-request-timeout (or timeout 1.5))) (setf (eglot--shutdown-requested server) t) (eglot--request server :shutdown nil) ;; this one is supposed to always fail, hence ignore-errors From b21929955d9fe59ffc79a6759f1b8887bb7699cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 13 Jun 2018 18:46:19 +0100 Subject: [PATCH 226/771] Fix a bug when eglot--request times out * eglot.el (eglot--request): Better timeout message. (eglot--async-request): Must return the timer. --- lisp/progmodes/eglot.el | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e438c492dd9..7bef2ec08e1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -737,7 +737,7 @@ happens, the original timer keeps counting). Return (ID TIMER)." (setf (eglot--status server) `(,message t)) server `(:message "error ignored, status set" :id ,id :error ,code))) - (or timer (funcall make-timer))) + (setq timer (or timer (funcall make-timer)))) (eglot--pending-continuations server)) (list id timer))) @@ -758,7 +758,10 @@ DEFERRED is passed to `eglot--async-request', which see." (eglot--async-request server method params :success-fn (lambda (result) (throw done `(done ,result))) - :timeout-fn (lambda () (throw done '(error "Timed out"))) + :timeout-fn (lambda () (throw done + `(error + ,(format "Request id=%s timed out" + (car id-and-timer))))) :error-fn (eglot--lambda (&key code message _data) (throw done `(error ,(format "Ooops: %s: %s" code message)))) From 249dd2bd0dcfa4049dc6e82ac81e802233bd1947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 13 Jun 2018 18:48:41 +0100 Subject: [PATCH 227/771] * eglot.el (version): bump to 0.10 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7bef2ec08e1..ecfde4a022f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.9 +;; Version: 0.10 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From 504389181d15880179f668e4e11143be62487c9b Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sat, 16 Jun 2018 00:47:52 +0200 Subject: [PATCH 228/771] Implement formatting () Implement textDocument/formatting * eglot.el (eglot-format-buffer): New command to format current buffer. * eglot-tests.el (formatting): New test. GitHub-reference: https://github.com/joaotavora/eglot/issues/19 --- lisp/progmodes/eglot.el | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ecfde4a022f..98802dea60b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1382,6 +1382,35 @@ DUMMY is ignored." :workspace/symbol (list :query pattern))))) +(defun eglot-format-buffer () + "Format contents of current buffer." + (interactive) + (unless (eglot--server-capable :documentFormattingProvider) + (eglot--error "Server can't format!")) + (let* ((server (eglot--current-server-or-lose)) + (resp + (eglot--request + server + :textDocument/formatting + (list :textDocument (eglot--TextDocumentIdentifier) + :options (list + :tabSize tab-width + :insertSpaces (not indent-tabs-mode))) + :textDocument/formatting)) + (before-point + (buffer-substring (max (- (point) 60) (point-min)) (point))) + (after-point + (buffer-substring (point) (min (+ (point) 60) (point-max)))) + (regexp (and (not (bobp)) + (replace-regexp-in-string + "[\s\t\n\r]+" "[\s\t\n\r]+" + (concat "\\(" (regexp-quote after-point) "\\)"))))) + (when resp + (save-excursion + (eglot--apply-text-edits resp)) + (when (and (bobp) regexp (search-forward-regexp regexp nil t)) + (goto-char (match-beginning 1)))))) + (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) From e10f933d613e7acefbfc39f590b14d856f901acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 15 Jun 2018 23:56:29 +0100 Subject: [PATCH 229/771] Minor cleanup to new textdocument/formatting feature * README.md (Language feature): Tick textDocument/formatting * eglot.el (eglot-client-capabilities): Add formatting capability. Also move codeAction capability to the correct section. (eglot-format-buffer): Remove unused lexical variable before-point. --- lisp/progmodes/eglot.el | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98802dea60b..ac17fb895f8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -163,7 +163,6 @@ deferred to the future.") :workspace (list :applyEdit t :executeCommand `(:dynamicRegistration :json-false) - :codeAction `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges :json-false) :didChangeWatchesFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) @@ -179,6 +178,8 @@ deferred to the future.") :definition `(:dynamicRegistration :json-false) :documentSymbol `(:dynamicRegistration :json-false) :documentHighlight `(:dynamicRegistration :json-false) + :codeAction `(:dynamicRegistration :json-false) + :formatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (list)))) @@ -1397,8 +1398,6 @@ DUMMY is ignored." :tabSize tab-width :insertSpaces (not indent-tabs-mode))) :textDocument/formatting)) - (before-point - (buffer-substring (max (- (point) 60) (point-min)) (point))) (after-point (buffer-substring (point) (min (+ (point) 60) (point-max)))) (regexp (and (not (bobp)) From 8bb92096ef7cd22b3651ae67b8e737a0d6e718a1 Mon Sep 17 00:00:00 2001 From: Rami Chowdhury <460769+necaris@users.noreply.github.com> Date: Sat, 16 Jun 2018 18:59:57 -0400 Subject: [PATCH 230/771] Use gfm-mode for formatted strings () * eglot.el (eglot--format-markup): Use gfm-mode instead of markdown-mode. Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/20 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ac17fb895f8..1aa3661d4d2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -847,7 +847,7 @@ If optional MARKER, return a marker instead" "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) (if (stringp markup) (list (string-trim markup) - (intern "markdown-mode")) + (intern "gfm-mode")) (list (plist-get markup :value) (intern (concat (plist-get markup :language) "-mode" )))))) (with-temp-buffer From 30d3874723f7f56615a32d44ca2e19870f783bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 20 Jun 2018 18:47:45 +0100 Subject: [PATCH 231/771] Improve eglot-ensure and mention it in readme.md * README.md (Installation and Usage): Mention eglot-ensure. * eglot.el (eglot-ensure): No-op for non-file buffers. (eglot--connect): Don't fallback to 'eglot-lsp-server here. (eglot--guess-contact): Error if something can't be guessed. GitHub-reference: close https://github.com/joaotavora/eglot/issues/25 --- lisp/progmodes/eglot.el | 44 ++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1aa3661d4d2..ecbd509b003 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -299,7 +299,7 @@ class SERVER-CLASS." server connect-success) (setq server (make-instance - (or server-class 'eglot-lsp-server) + server-class :process proc :major-mode managed-major-mode :project project :contact contact :name name :project-nickname nickname @@ -341,23 +341,28 @@ class SERVER-CLASS." (defun eglot--guess-contact (&optional interactive) "Helper for `eglot'. -Return (MANAGED-MODE PROJECT CONTACT CLASS). -If INTERACTIVE, maybe prompt user." +Return (MANAGED-MODE PROJECT CLASS CONTACT). If INTERACTIVE is +non-nil, maybe prompt user, else error as soon as something can't +be guessed." (let* ((guessed-mode (if buffer-file-name major-mode)) (managed-mode (cond - ((or (>= (prefix-numeric-value current-prefix-arg) 16) - (not guessed-mode)) + ((and interactive + (or (>= (prefix-numeric-value current-prefix-arg) 16) + (not guessed-mode))) (intern (completing-read "[eglot] Start a server to manage buffers of what major mode? " (mapcar #'symbol-name (eglot--all-major-modes)) nil t (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) + ((not guessed-mode) + (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) (project (or (project-current) `(transient . ,default-directory))) (guess (cdr (assoc managed-mode eglot-server-programs))) - (class (and (consp guess) (symbolp (car guess)) - (prog1 (car guess) (setq guess (cdr guess))))) + (class (or (and (consp guess) (symbolp (car guess)) + (prog1 (car guess) (setq guess (cdr guess)))) + 'eglot-lsp-server)) (program (and (listp guess) (stringp (car guess)) (car guess))) (base-prompt (and interactive @@ -374,16 +379,18 @@ If INTERACTIVE, maybe prompt user." (format ", but I can't find `%s' in PATH!" program) "\n" base-prompt))))) (contact - (if prompt - (let ((s (read-shell-command - prompt - (if program (combine-and-quote-strings guess)) - 'eglot-command-history))) - (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" - (string-trim s)) - (list (match-string 1 s) (string-to-number (match-string 2 s))) - (split-string-and-unquote s))) - guess))) + (or (and prompt + (let ((s (read-shell-command + prompt + (if program (combine-and-quote-strings guess)) + 'eglot-command-history))) + (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" + (string-trim s)) + (list (match-string 1 s) + (string-to-number (match-string 2 s))) + (split-string-and-unquote s)))) + guess + (eglot--error "Couldn't guess for `%s'!" managed-mode)))) (list managed-mode project class contact))) ;;;###autoload @@ -470,7 +477,8 @@ INTERACTIVE is t if called interactively." (eglot--name server) major-mode (eglot--project-nickname server))))))) - (add-hook 'post-command-hook #'maybe-connect 'append nil)))) + (when buffer-file-name + (add-hook 'post-command-hook #'maybe-connect 'append nil))))) (defun eglot--process-sentinel (proc change) "Called when PROC undergoes CHANGE." From 29b44f7ec1dc6a094e751ec0af6fb950c7858a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 20 Jun 2018 19:05:25 +0100 Subject: [PATCH 232/771] Guess server for js2-mode and rjsx-mode * eglot.el (eglot-server-programs): Add entries for js2-mode and rjsx-mode. Coalesce entries for c++ and c-mode. Improve docstring. (eglot--guess-contact): Allow lists are keys in eglot-server-programs. GitHub-reference: close https://github.com/joaotavora/eglot/issues/26 --- lisp/progmodes/eglot.el | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ecbd509b003..447e8c17f25 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -78,15 +78,19 @@ (defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) (python-mode . ("pyls")) - (js-mode . ("javascript-typescript-stdio")) + ((js-mode + js2-mode + rjsx-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) - (c++-mode . (eglot-cquery "cquery")) - (c-mode . (eglot-cquery "cquery")) + ((c++-mode + c-mode) . (eglot-cquery "cquery")) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . SPEC) pair. MAJOR-MODE is a -mode symbol. SPEC is +mode symbol, or a list of mode symbols. The associated SPEC +specifies how to start a server for managing buffers of those +modes. SPEC can be: * In the most common case, a list of strings (PROGRAM [ARGS...]). PROGRAM is called with ARGS and is expected to serve LSP requests @@ -359,7 +363,10 @@ be guessed." (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) (project (or (project-current) `(transient . ,default-directory))) - (guess (cdr (assoc managed-mode eglot-server-programs))) + (guess (cdr (assoc managed-mode eglot-server-programs + (lambda (m1 m2) + (or (eq m1 m2) + (and (listp m1) (memq m2 m1))))))) (class (or (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) From 4f346ba25036ca5525ab298aa34a3bcb4f4807f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 20 Jun 2018 19:29:30 +0100 Subject: [PATCH 233/771] Simplify eglot-format-buffer Use replace-buffer-contents, as suggested by mkcms , but do it in eglot--apply-text-edits, where it benefits all its users. * README.md (Commands and keybindings): Mention eglot-format-buffer. * eglot.el (eglot-format-buffer): Don't try to heuristically preserve point here. (eglot--apply-text-edits): Use replace-buffer-contents. * eglot-tests.el (formatting): adjust test to strictly check for point position. GitHub-reference: per https://github.com/joaotavora/eglot/issues/22 --- lisp/progmodes/eglot.el | 46 +++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 447e8c17f25..0f366c41a74 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1403,27 +1403,14 @@ DUMMY is ignored." (interactive) (unless (eglot--server-capable :documentFormattingProvider) (eglot--error "Server can't format!")) - (let* ((server (eglot--current-server-or-lose)) - (resp - (eglot--request - server - :textDocument/formatting - (list :textDocument (eglot--TextDocumentIdentifier) - :options (list - :tabSize tab-width - :insertSpaces (not indent-tabs-mode))) - :textDocument/formatting)) - (after-point - (buffer-substring (point) (min (+ (point) 60) (point-max)))) - (regexp (and (not (bobp)) - (replace-regexp-in-string - "[\s\t\n\r]+" "[\s\t\n\r]+" - (concat "\\(" (regexp-quote after-point) "\\)"))))) - (when resp - (save-excursion - (eglot--apply-text-edits resp)) - (when (and (bobp) regexp (search-forward-regexp regexp nil t)) - (goto-char (match-beginning 1)))))) + (eglot--apply-text-edits + (eglot--request + (eglot--current-server-or-lose) + :textDocument/formatting + (list :textDocument (eglot--TextDocumentIdentifier) + :options (list :tabSize tab-width + :insertSpaces + (if indent-tabs-mode :json-false t)))))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." @@ -1595,12 +1582,17 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (eglot--widening - (mapc (pcase-lambda (`(,newText ,beg . ,end)) - (goto-char beg) (delete-region beg end) (insert newText)) - (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) - edits))) + (mapc (pcase-lambda (`(,newText ,beg . ,end)) + (save-restriction + (narrow-to-region beg end) + (let ((source (current-buffer))) + (with-temp-buffer + (insert newText) + (let ((temp (current-buffer))) + (with-current-buffer source (replace-buffer-contents temp))))))) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits)) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) From 6fc53b840d22c08ca444efd4bf693a25e2bb26e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 21 Jun 2018 13:58:48 +0100 Subject: [PATCH 234/771] Defer textdocument/formatting requests * eglot.el (eglot-format-buffer): Pass DEFERRED to eglot--request. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0f366c41a74..e96a3e68e23 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1410,7 +1410,8 @@ DUMMY is ignored." (list :textDocument (eglot--TextDocumentIdentifier) :options (list :tabSize tab-width :insertSpaces - (if indent-tabs-mode :json-false t)))))) + (if indent-tabs-mode :json-false t))) + :textDocument/formatting))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." From 31b21e371da5676fbdac4a3f352eceb657a62180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 21 Jun 2018 14:53:50 +0100 Subject: [PATCH 235/771] Apply text edits atomically As suggested by mkcms , but do it in eglot--apply-text-edits, where it benefits all its users. * eglot.el (eglot--apply-text-edits): Use atomic-change-group. GitHub-reference: per https://github.com/joaotavora/eglot/issues/22 --- lisp/progmodes/eglot.el | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e96a3e68e23..489e3658b11 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1583,17 +1583,18 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (or (not version) (equal version eglot--versioned-identifier)) (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) - (mapc (pcase-lambda (`(,newText ,beg . ,end)) - (save-restriction - (narrow-to-region beg end) - (let ((source (current-buffer))) - (with-temp-buffer - (insert newText) - (let ((temp (current-buffer))) - (with-current-buffer source (replace-buffer-contents temp))))))) - (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) - edits)) + (atomic-change-group + (mapc (pcase-lambda (`(,newText ,beg . ,end)) + (save-restriction + (narrow-to-region beg end) + (let ((source (current-buffer))) + (with-temp-buffer + (insert newText) + (let ((temp (current-buffer))) + (with-current-buffer source (replace-buffer-contents temp))))))) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits))) (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) (defun eglot--apply-workspace-edit (wedit &optional confirm) From 1e49d2f3b45be49278f13a4b9fbb5dab2707952a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 21 Jun 2018 17:20:20 +0100 Subject: [PATCH 236/771] Report progress when applying edits Use make-progress-reporter in eglot--apply-text-edits As suggested by mkcms , but do it in eglot--apply-text-edits, where it benefits all its users. * eglot.el (eglot--apply-text-edits): Use a progress reporter. Fix marker point recovery. GitHub-reference: close https://github.com/joaotavora/eglot/issues/23 --- lisp/progmodes/eglot.el | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 489e3658b11..9e686eece63 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1584,18 +1584,27 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) (atomic-change-group - (mapc (pcase-lambda (`(,newText ,beg . ,end)) - (save-restriction - (narrow-to-region beg end) + (let* ((howmany (length edits)) + (reporter (make-progress-reporter + (format "[eglot] applying %s edits to `%s'..." + howmany (current-buffer)) + 0 howmany)) + (done 0)) + (mapc (pcase-lambda (`(,newText ,beg . ,end)) (let ((source (current-buffer))) (with-temp-buffer (insert newText) (let ((temp (current-buffer))) - (with-current-buffer source (replace-buffer-contents temp))))))) - (mapcar (eglot--lambda (&key range newText) - (cons newText (eglot--range-region range 'markers))) - edits))) - (eglot--message "%s: Performed %s edits" (current-buffer) (length edits))) + (with-current-buffer source + (save-excursion + (save-restriction + (narrow-to-region beg end) + (replace-buffer-contents temp))) + (progress-reporter-update reporter (cl-incf done))))))) + (mapcar (eglot--lambda (&key range newText) + (cons newText (eglot--range-region range 'markers))) + edits)) + (progress-reporter-done reporter)))) (defun eglot--apply-workspace-edit (wedit &optional confirm) "Apply the workspace edit WEDIT. If CONFIRM, ask user first." From 81b7c8e7d736d4118cb52afcaf6a46a55646fabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 21 Jun 2018 17:48:10 +0100 Subject: [PATCH 237/771] Apply text edits as a single undoable edit As suggested by mkcms , but do it in eglot--apply-text-edits, where it benefits all its users. Also, just using undo-boundary is not enough, one needs undo-amalgamate-change-group to mess with the boundaries already in buffer-undo-list. * eglot.el (eglot--apply-text-edits): Use undo-amalgamate-change-group. GitHub-reference: close https://github.com/joaotavora/eglot/issues/22 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9e686eece63..2fdf433c55a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1584,7 +1584,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--error "Edits on `%s' require version %d, you have %d" (current-buffer) version eglot--versioned-identifier)) (atomic-change-group - (let* ((howmany (length edits)) + (let* ((change-group (prepare-change-group)) + (howmany (length edits)) (reporter (make-progress-reporter (format "[eglot] applying %s edits to `%s'..." howmany (current-buffer)) @@ -1604,6 +1605,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapcar (eglot--lambda (&key range newText) (cons newText (eglot--range-region range 'markers))) edits)) + (undo-amalgamate-change-group change-group) (progress-reporter-done reporter)))) (defun eglot--apply-workspace-edit (wedit &optional confirm) From 7826b265a0ecd9357719b2fb9491c9bcb517d4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 21 Jun 2018 23:32:14 +0100 Subject: [PATCH 238/771] Empty ranges are valid in lsp The previous hack in eglot--range-region, designed to appease cquery's occasional practice of publishing diagnostics with empty regions, was moved to the proper notification handler. Reported by mkcms . * eglot.el (eglot--range-region): Allow empty ranges, which are allowed in LSP. (eglot-handle-notification :textDocument/publishDiagnostics): Maybe fallback to flymake-diag-region here. GitHub-reference: close https://github.com/joaotavora/eglot/issues/27 --- lisp/progmodes/eglot.el | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2fdf433c55a..f4a03da7e6c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -896,10 +896,7 @@ If optional MARKERS, make markers." (let* ((st (plist-get range :start)) (beg (eglot--lsp-position-to-point st markers)) (end (eglot--lsp-position-to-point (plist-get range :end) markers))) - ;; Fallback to `flymake-diag-region' if server botched the range - (if (/= beg end) (cons beg end) (flymake-diag-region - (current-buffer) (plist-get st :line) - (1- (plist-get st :character)))))) + (cons beg end))) ;;; Minor modes @@ -1125,7 +1122,18 @@ Don't leave this function with the server still running." _code source message) diag-spec (setq message (concat source ": " message)) - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (pcase-let + ((`(,beg . ,end) (eglot--range-region range))) + ;; Fallback to `flymake-diag-region' if server + ;; botched the range + (if (= beg end) + (let* ((st (plist-get range :start)) + (diag-region + (flymake-diag-region + (current-buffer) (plist-get st :line) + (1- (plist-get st :character))))) + (setq beg (car diag-region) + end (cdr diag-region)))) (eglot--make-diag (current-buffer) beg end (cond ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) From d370eeb128ab4f2a3bf0e17c9bd7a19be74fb11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 22 Jun 2018 16:51:42 +0100 Subject: [PATCH 239/771] * eglot.el (version): bump to 0.11 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f4a03da7e6c..62116d33207 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 0.10 +;; Version: 0.11 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From 0c61c1b4a9f8dc358209ed2b9844b813a36215a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 23 Jun 2018 17:00:57 +0100 Subject: [PATCH 240/771] Implement tcp autostart/autoconnect (and support ruby's solargraph) * README.md (Installation and usage): Mention support for Solargraph (Connecting via TCP): New section (Connecting automatically): New section * eglot.el (eglot-server-programs): Add ruby-mode. Overhaul docstring. (eglot-lsp-server): Add inferior-process slot. (eglot--on-shutdown): Kill any autostarted inferior-process (eglot--guess-contact): Allow prompting with :autoport parameter. (eglot--connect): Consider :autoport case. (eglot--inferior-bootstrap): New helper. --- lisp/progmodes/eglot.el | 124 +++++++++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 22 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7a4468d2c33..6d0541a1e69 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -85,32 +85,42 @@ (sh-mode . ("bash-language-server" "start")) ((c++-mode c-mode) . (eglot-cquery "cquery")) + (ruby-mode + . ("solagraph" "socket" "--port" + :autoport)) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated -CONTACT specifies how to start a server for managing buffers of -those modes. CONTACT can be: +CONTACT specifies how to connect to a server for managing buffers +of those modes. CONTACT can be: * In the most common case, a list of strings (PROGRAM [ARGS...]). -PROGRAM is called with ARGS and is expected to serve LSP requests -over the standard input/output channels. + PROGRAM is called with ARGS and is expected to serve LSP requests + over the standard input/output channels. -* A list (HOST PORT [ARGS...]) where HOST is a string and PORT is -a positive integer number for connecting to a server via TCP. -Remaining ARGS are passed to `open-network-stream' for upgrading -the connection with encryption or other capabilities. +* A list (HOST PORT [TCP-ARGS...]) where HOST is a string and PORT is + na positive integer number for connecting to a server via TCP. + Remaining ARGS are passed to `open-network-stream' for + upgrading the connection with encryption or other capabilities. + +* A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereby a + combination of the two previous options is used.. First, an + attempt is made to find an available server port, then PROGRAM + is launched with ARGS; the `:autoport' keyword substituted for + that number; and MOREARGS. Eglot then attempts to to establish + a TCP connection to that port number on the localhost. * A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol -designating a subclass of `eglot-lsp-server', for representing -experimental LSP servers. INITARGS is a keyword-value plist used -to initialize CLASS-NAME, or a plain list interpreted as the -previous descriptions of CONTACT, in which case it is converted -to produce a plist with a suitable :PROCESS initarg to -CLASS-NAME. The class `eglot-lsp-server' descends -`jsonrpc-process-connection', which you should see for semantics -of the mandatory :PROCESS argument.") + designating a subclass of `eglot-lsp-server', for representing + experimental LSP servers. INITARGS is a keyword-value plist + used to initialize CLASS-NAME, or a plain list interpreted as + the previous descriptions of CONTACT, in which case it is + converted to produce a plist with a suitable :PROCESS initarg + to CLASS-NAME. The class `eglot-lsp-server' descends + `jsonrpc-process-connection', which you should see for the + semantics of the mandatory :PROCESS argument.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -205,8 +215,11 @@ lasted more than that many seconds." :documentation "List of buffers managed by server." :accessor eglot--managed-buffers) (saved-initargs - :documentation "Saved initargs for reconnection purposes" - :accessor eglot--saved-initargs)) + :documentation "Saved initargs for reconnection purposes." + :accessor eglot--saved-initargs) + (inferior-process + :documentation "Server subprocess started automatically." + :accessor eglot--inferior-process)) :documentation "Represents a server. Wraps a process for LSP communication.") @@ -244,6 +257,9 @@ Don't leave this function with the server still running." (maphash (lambda (_id watches) (mapcar #'file-notify-rm-watch watches)) (eglot--file-watches server)) + ;; Kill any autostarted inferior processes + (when-let (proc (eglot--inferior-process server)) + (delete-process proc)) ;; Sever the project/server relationship for `server' (setf (gethash (eglot--project server) eglot--servers-by-project) (delq server @@ -297,7 +313,11 @@ be guessed." (program (and (listp guess) (stringp (car guess)) (car guess))) (base-prompt (and interactive - "[eglot] Enter program to execute (or :): ")) + "Enter program to execute (or :): ")) + (program-guess + (and program + (combine-and-quote-strings (cl-subst ":autoport:" + :autoport guess)))) (prompt (and base-prompt (cond (current-prefix-arg base-prompt) @@ -306,20 +326,22 @@ be guessed." managed-mode base-prompt)) ((and program (not (executable-find program))) (concat (format "[eglot] I guess you want to run `%s'" - (combine-and-quote-strings guess)) + program-guess) (format ", but I can't find `%s' in PATH!" program) "\n" base-prompt))))) (contact (or (and prompt (let ((s (read-shell-command prompt - (if program (combine-and-quote-strings guess)) + program-guess 'eglot-command-history))) (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" (string-trim s)) (list (match-string 1 s) (string-to-number (match-string 2 s))) - (split-string-and-unquote s)))) + (cl-subst + :autoport ":autoport:" (split-string-and-unquote s) + :test #'equal)))) guess (eglot--error "Couldn't guess for `%s'!" managed-mode)))) (list managed-mode project class contact))) @@ -429,6 +451,7 @@ This docstring appeases checkdoc, that's all." (let* ((nickname (file-name-base (directory-file-name (car (project-roots project))))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) + autostart-inferior-process (initargs (cond ((keywordp (car contact)) contact) ((integerp (cadr contact)) @@ -437,6 +460,14 @@ This docstring appeases checkdoc, that's all." readable-name nil (car contact) (cadr contact) (cddr contact))))) + ((and (stringp (car contact)) (memq :autoport contact)) + `(:process ,(lambda () + (pcase-let ((`(,connection . ,inferior) + (eglot--inferior-bootstrap + readable-name + contact))) + (setq autostart-inferior-process inferior) + connection)))) ((stringp (car contact)) `(:process ,(lambda () (make-process @@ -463,6 +494,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) + (setf (eglot--inferior-process server) autostart-inferior-process) (push server (gethash project eglot--servers-by-project)) (run-hook-with-args 'eglot-connect-hook server) (unwind-protect @@ -495,6 +527,54 @@ This docstring appeases checkdoc, that's all." (when (and (not success) (jsonrpc-running-p server)) (eglot-shutdown server))))) +(defun eglot--inferior-bootstrap (name contact &optional connect-args) + "Use CONTACT to start a server, then connect to it. +Return a cons of two process objects (CONNECTION . INFERIOR). +Name both based on NAME. +CONNECT-ARGS are passed as additional arguments to +`open-network-stream'." + (let* ((port-probe (make-network-process :name "eglot-port-probe-dummy" + :server t + :host "localhost" + :service 0)) + (port-number (unwind-protect + (process-contact port-probe :service) + (delete-process port-probe))) + inferior connection) + (unwind-protect + (progn + (setq inferior + (make-process + :name (format "autostart-inferior-%s" name) + :stderr (format "*%s stderr*" name) + :command (cl-subst + (format "%s" port-number) :autoport contact))) + (setq connection + (cl-loop + repeat 10 for i from 1 + do (accept-process-output nil 0.5) + while (process-live-p inferior) + do (eglot--message + "Trying to connect to localhost and port %s (attempt %s)" + port-number i) + thereis (ignore-errors + (apply #'open-network-stream + (format "autoconnect-%s" name) + nil + "localhost" port-number connect-args)))) + (cons connection inferior)) + (cond ((and (process-live-p connection) + (process-live-p inferior)) + (eglot--message "Done, connected to %s!" port-number)) + (t + (when inferior (delete-process inferior)) + (when connection (delete-process connection)) + (eglot--error "Could not start and connect to server%s" + (if inferior + (format " started with %s" + (process-command inferior)) + "!"))))))) + ;;; Helpers (move these to API?) ;;; From 5556a341ed02cccf20742dce32d65b5e9a0a1e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 23 Jun 2018 17:43:23 +0100 Subject: [PATCH 241/771] Fix some rather silly bugs in some interactive specs * eglot.el (eglot-events-buffer, eglot-stderr-buffer) (eglot-forget-pending-continuations): Fix interactive specs. --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d0541a1e69..430a0164611 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -430,17 +430,17 @@ INTERACTIVE is t if called interactively." (defun eglot-events-buffer (server) "Display events buffer for SERVER." - (interactive (eglot--current-server-or-lose)) + (interactive (list (eglot--current-server-or-lose))) (display-buffer (jsonrpc-events-buffer server))) (defun eglot-stderr-buffer (server) "Display stderr buffer for SERVER." - (interactive (eglot--current-server-or-lose)) + (interactive (list (eglot--current-server-or-lose))) (display-buffer (jsonrpc-stderr-buffer server))) (defun eglot-forget-pending-continuations (server) "Forget pending requests for SERVER." - (interactive (eglot--current-server-or-lose)) + (interactive (list (eglot--current-server-or-lose))) (jsonrpc-forget-pending-continuations server)) (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") From d40cbb99a5f859474b54f7d8d2fed05acc5e3ef1 Mon Sep 17 00:00:00 2001 From: Ricardo Martins <1706+meqif@users.noreply.github.com> Date: Mon, 25 Jun 2018 12:51:10 +0100 Subject: [PATCH 242/771] Fix typo in the solargraph server program Copyright-paperwork-exempt: yes * eglot.el (eglot-server-programs): Fix typo. GitHub-reference: close https://github.com/joaotavora/eglot/issues/30 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 430a0164611..ce335433516 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -86,7 +86,7 @@ ((c++-mode c-mode) . (eglot-cquery "cquery")) (ruby-mode - . ("solagraph" "socket" "--port" + . ("solargraph" "socket" "--port" :autoport)) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php"))) From 5b66bec822823636e09dfef74919c733ded9f1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 25 Jun 2018 13:02:03 +0100 Subject: [PATCH 243/771] Unbreak basic imenu functionality * eglot.el (eglot--managed-mode): Add missing quote to imenu-create-index-function. GitHub-reference: per https://github.com/joaotavora/eglot/issues/31 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ce335433516..f8574e9428e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -692,7 +692,7 @@ If optional MARKERS, make markers." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (add-function :around (local imenu-create-index-function) #'eglot-imenu)) + (add-function :around (local 'imenu-create-index-function) #'eglot-imenu)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) @@ -705,7 +705,7 @@ If optional MARKERS, make markers." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (remove-function (local imenu-create-index-function) #'eglot-imenu) + (remove-function (local 'imenu-create-index-function) #'eglot-imenu) (setq eglot--current-flymake-report-fn nil)))) (defun eglot--managed-mode-onoff (server arg) From 9b3ef1315ce3d0cb3762a92158a7241af31dd6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 25 Jun 2018 13:12:55 +0100 Subject: [PATCH 244/771] Unbreak imenu for cquery servers (and probably more) * eglot.el (eglot-imenu): Don't try to make a group for symbols without kind. GitHub-reference: close https://github.com/joaotavora/eglot/issues/31 --- lisp/progmodes/eglot.el | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f8574e9428e..88746a72823 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1349,8 +1349,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) (append - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries) + (cl-remove nil + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries) + :key #'car) entries)) (funcall oldfun))) From 973b0255229f0d5fa9aaf141743584c969293d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 25 Jun 2018 17:37:43 +0100 Subject: [PATCH 245/771] Cache buffer's managing server * eglot.el (eglot--cached-current-server): New variable. (eglot--managed-mode-onoff): Set it. (eglot--current-server): Read it. (eglot--maybe-activate-editing-mode): Add assertion. GitHub-reference: close https://github.com/joaotavora/eglot/issues/32 --- lisp/progmodes/eglot.el | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 88746a72823..01658f9343e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -708,23 +708,33 @@ If optional MARKERS, make markers." (remove-function (local 'imenu-create-index-function) #'eglot-imenu) (setq eglot--current-flymake-report-fn nil)))) +(defvar-local eglot--cached-current-server nil + "A cached reference to the current EGLOT server. +Reset in `eglot--managed-mode-onoff'.") + (defun eglot--managed-mode-onoff (server arg) "Proxy for function `eglot--managed-mode' with ARG and SERVER." (eglot--managed-mode arg) (let ((buf (current-buffer))) - (if eglot--managed-mode - (cl-pushnew buf (eglot--managed-buffers server)) - (setf (eglot--managed-buffers server) - (delq buf (eglot--managed-buffers server)))))) + (cond (eglot--managed-mode + (setq eglot--cached-current-server server) + (cl-pushnew buf (eglot--managed-buffers server))) + (t + (setq eglot--cached-current-server nil) + (setf (eglot--managed-buffers server) + (delq buf (eglot--managed-buffers server))))))) (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) (defun eglot--current-server () "Find the current logical EGLOT server." - (let* ((probe (or (project-current) `(transient . ,default-directory)))) - (cl-find major-mode (gethash probe eglot--servers-by-project) - :key #'eglot--major-mode))) + (or + eglot--cached-current-server + (let* ((probe (or (project-current) + `(transient . ,default-directory)))) + (cl-find major-mode (gethash probe eglot--servers-by-project) + :key #'eglot--major-mode)))) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." @@ -738,6 +748,12 @@ If optional MARKERS, make markers." "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." + (unless server + (when eglot--cached-current-server + (display-warning + :eglot "`eglot--cached-current-server' is non-nil, but it should be!\n\ +Please report this as a possible bug.") + (setq eglot--cached-current-server nil))) ;; Called even when revert-buffer-in-progress-p (let* ((cur (and buffer-file-name (eglot--current-server))) (server (or (and (null server) cur) (and server (eq server cur) cur)))) From 2b071ccba5c4f88fd0a66c505110d5081d695631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 28 Jun 2018 23:30:39 +0100 Subject: [PATCH 246/771] Bind default-directory when launching servers Apparently, not doing so trips some servers, like Scala's. * eglot.el (eglot--connect): Bind default-directory. GitHub-reference: close https://github.com/joaotavora/eglot/issues/33 --- lisp/progmodes/eglot.el | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 01658f9343e..50733f74581 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -448,8 +448,8 @@ INTERACTIVE is t if called interactively." (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." - (let* ((nickname (file-name-base (directory-file-name - (car (project-roots project))))) + (let* ((default-directory (car (project-roots project))) + (nickname (file-name-base (directory-file-name default-directory))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) autostart-inferior-process (initargs @@ -504,10 +504,8 @@ This docstring appeases checkdoc, that's all." :initialize (list :processId (unless (eq (jsonrpc-process-type server) 'network) (emacs-pid)) - :rootPath (expand-file-name - (car (project-roots project))) - :rootUri (eglot--path-to-uri - (car (project-roots project))) + :rootPath (expand-file-name default-directory) + :rootUri (eglot--path-to-uri default-directory) :initializationOptions (eglot-initialization-options server) :capabilities (eglot-client-capabilities server))) (setf (eglot--capabilities server) capabilities) From 1a58481719f63c3e77832f7c8474ce745a05ad4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 1 Jul 2018 22:49:40 +0100 Subject: [PATCH 247/771] Inhibit auto-reconnect until connection is established Otherwise, a server that crashes on startup is enough to throw Eglot into a reconnection infloop. * eglot.el (eglot-lsp-server): Initialize "inhibit-autoreconnect" slot to t. GitHub-reference: close https://github.com/joaotavora/eglot/issues/36 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 50733f74581..8cad3ad1b58 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -206,6 +206,7 @@ lasted more than that many seconds." :documentation "List (ID DOING-WHAT DONE-P) representing server progress." :initform `(nil nil t) :accessor eglot--spinner) (inhibit-autoreconnect + :initform t :documentation "Generalized boolean inhibiting auto-reconnection if true." :accessor eglot--inhibit-autoreconnect) (file-watches From d599dfd79f17eea5e4a9708dd29336149ac386ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 2 Jul 2018 23:09:27 +0100 Subject: [PATCH 248/771] Handle outrageously large and buggy line numbers * eglot.el (eglot--lsp-position-to-point): Truncate line number to most-positive-fixnum. GitHub-reference: close https://github.com/joaotavora/eglot/issues/34 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8cad3ad1b58..509fe6c88dc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -602,7 +602,8 @@ CONNECT-ARGS are passed as additional arguments to "Convert LSP position POS-PLIST to Emacs point. If optional MARKER, return a marker instead" (save-excursion (goto-char (point-min)) - (forward-line (plist-get pos-plist :line)) + (forward-line (min most-positive-fixnum + (plist-get pos-plist :line))) (forward-char (min (plist-get pos-plist :character) (- (line-end-position) (line-beginning-position)))) From d8a14e9ea84a7636f230ed4a9870b064f48d98fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 6 Jul 2018 18:39:20 +0100 Subject: [PATCH 249/771] Unbreak completion when no possible annotation * eglot.el (eglot-completion-at-point): Handle case where no doc, detail or kind. GitHub-reference: close https://github.com/joaotavora/eglot/issues/37 --- lisp/progmodes/eglot.el | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 509fe6c88dc..fd633388452 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1225,11 +1225,14 @@ DUMMY is ignored." (lambda (obj) (cl-destructuring-bind (&key detail documentation kind &allow-other-keys) (text-properties-at 0 obj) - (concat " " (propertize - (or (and documentation - (replace-regexp-in-string "\n.*" "" documentation)) - detail (cdr (assoc kind eglot--kind-names))) - 'face 'font-lock-function-name-face)))) + (let ((annotation + (or (and documentation + (replace-regexp-in-string "\n.*" "" documentation)) + detail + (cdr (assoc kind eglot--kind-names))))) + (when annotation + (concat " " (propertize annotation + 'face 'font-lock-function-name-face)))))) :display-sort-function (lambda (items) (sort items (lambda (a b) From 6aeaf37c9b54abfba72eeca853255f16194accad Mon Sep 17 00:00:00 2001 From: Ricardo Martins Date: Sun, 8 Jul 2018 17:02:32 +0100 Subject: [PATCH 250/771] Format documentation in completion annotations Fixes an issue with the latest RLS, where the server returns a plist instead of a plain string as documentation for completion candidates, which broke the `annotation-function`. This change was introduced by https://github.com/rust-lang-nursery/rls/commit/206a9fb41e837333d0e67187a6a9fe24868b77a4 Copyright-paperwork-exempt: yes * eglot.el (eglot-completion-at-point): Use eglot--format-markup --- lisp/progmodes/eglot.el | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fd633388452..396590f6fd0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1216,10 +1216,10 @@ DUMMY is ignored." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) @@ -1227,7 +1227,8 @@ DUMMY is ignored." (text-properties-at 0 obj) (let ((annotation (or (and documentation - (replace-regexp-in-string "\n.*" "" documentation)) + (replace-regexp-in-string + "\n.*" "" (eglot--format-markup documentation))) detail (cdr (assoc kind eglot--kind-names))))) (when annotation From 03caf3bb27603d2c75269ed6f2c5db85da115cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 9 Jul 2018 00:33:50 +0100 Subject: [PATCH 251/771] * eglot.el (eglot-completion-at-point): fix broken indentation --- lisp/progmodes/eglot.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 396590f6fd0..26c52d31f30 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1216,10 +1216,10 @@ DUMMY is ignored." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (let ((insert (or insertText label))) + (add-text-properties 0 1 all insert) + (put-text-property 0 1 'eglot--lsp-completion all insert) + insert)) items)))) :annotation-function (lambda (obj) From f52846f56f143788b8e235ba24f050e32486fb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 9 Jul 2018 22:23:57 +0100 Subject: [PATCH 252/771] Jsonrpc.el is now a gnu elpa depedency * Makefile (ELFILES): Don't include jsonrpc. (jsonrpc-check): Remove target. (check): Don't run jsonrpc-check * README.md (either): Mention "packaged in a single file" again * eglot.el (Package-Requires): Require jsonrpc 1.0.0 (Version): Bump to 1.1 * jsonrpc.el: Remove * jsonrpc-tests.el: Remove --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 26c52d31f30..8d61bbbce37 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,12 +2,12 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.0 +;; Version: 1.1 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.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 From 46de6683a7e827b3fb3d273605b3461075038c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 11 Jul 2018 09:46:14 +0100 Subject: [PATCH 253/771] Handle experimental/unknown server methods gracefully * eglot.el (eglot-handle-notification t t, eglot-handle-request t t): Add default handlers for unknown methods. (eglot-handle-notification $cquery/progress) (eglot-handle-notification $cquery/setInactiveRegions) (eglot-handle-notification $cquery/publishSemanticHighlighting): Remove these no-ops. GitHub-reference: close https://github.com/joaotavora/eglot/issues/39 --- lisp/progmodes/eglot.el | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8d61bbbce37..77f8260b229 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -856,6 +856,16 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; +(cl-defmethod eglot-handle-notification + (_server method &key) + "Handle unknown notification" + (eglot--warn "Server sent unknown notification method `%s'" method)) + +(cl-defmethod eglot-handle-request + (_server method &key) + "Handle unknown request" + (jsonrpc-error "Unknown request method `%s'" method)) + (cl-defmethod eglot-handle-notification (_server (_method (eql window/showMessage)) &key type message) "Handle notification window/showMessage" @@ -1582,21 +1592,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (list :cacheDirectory (file-name-as-directory cache) :progressReportFrequencyMs -1))) -(cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql $cquery/progress)) - &rest counts &key _activeThreads &allow-other-keys) - "No-op for noisy $cquery/progress extension") - -(cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql $cquery/setInactiveRegions)) - &key _uri _inactiveRegions &allow-other-keys) - "No-op for unsupported $cquery/setInactiveRegions extension") - -(cl-defmethod eglot-handle-notification - ((_server eglot-cquery) (_method (eql $cquery/publishSemanticHighlighting)) - &key _uri _symbols &allow-other-keys) - "No-op for unsupported $cquery/publishSemanticHighlighting extension") - ;; FIXME: A horrible hack of Flymake's insufficient API that must go ;; into Emacs master, or better, 26.2 From 228ddf368de316667905448dca847bb760cf2d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 12 Jul 2018 00:30:32 +0100 Subject: [PATCH 254/771] Implement workspace/didchangeconfiguration () * README.md (Supported Protocol Features, Commands and keybindings): mention workspace/didChangeConfiguration. * eglot.el (eglot-server-initialized-hook): New hook. (eglot--connect): Run it. (eglot-workspace-configuration): New variable. (eglot-signal-didChangeConfiguration): New command. GitHub-reference: close https://github.com/joaotavora/eglot/issues/29 GitHub-reference: close https://github.com/joaotavora/eglot/issues/40 --- lisp/progmodes/eglot.el | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 77f8260b229..8f9a43d93e8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -446,6 +446,11 @@ INTERACTIVE is t if called interactively." (defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") +(defvar eglot-server-initialized-hook + '(eglot-signal-didChangeConfiguration) + "Hook run after server is successfully initialized. +Each function is passed the server as an argument") + (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." @@ -514,6 +519,7 @@ This docstring appeases checkdoc, that's all." (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) (jsonrpc-notify server :initialized `(:__dummy__ t)) + (run-hook-with-args 'eglot-server-initialized-hook server) (setf (eglot--inhibit-autoreconnect server) (cond ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) @@ -1033,6 +1039,22 @@ Records START, END and PRE-CHANGE-LENGTH locally." (eglot--signal-textDocument/didChange)))) '((name . eglot--signal-textDocument/didChange))) +(defvar-local eglot-workspace-configuration () + "Alist of (SETTING . VALUE) entries configuring the LSP server. +Setting should be a keyword, value can be any value that can be +converted to JSON.") + +(defun eglot-signal-didChangeConfiguration (server) + "Send a `:workspace/didChangeConfiguration' signal to SERVER. +When called interactively, use the currently active server" + (interactive (list (eglot--current-server-or-lose))) + (jsonrpc-notify + server :workspace/didChangeConfiguration + (list + :settings + (cl-loop for (k . v) in eglot-workspace-configuration + collect k collect v)))) + (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when eglot--recent-changes From c959101180852aa2f46b2e51b21a36a76fcb8052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 20 Jul 2018 17:45:47 +0100 Subject: [PATCH 255/771] Robustify in the face of manual mode changes When manually changing the major-mode of a managed buffer, this sends a didClose and tears down Eglot-related stuff like if were killing the buffer. After changing the mode, we have to recheck that we are now not managed by another server (or by the same server, in case we changed the mode to be the same mode). * eglot.el (eglot-shutdown): Use eglot--with-live-buffer (eglot--on-shutdown): Use eglot--with-live-buffer (eglot--managed-mode): Use change-major-mode-hook. (eglot--managed-mode-onoff): Change protocol. Turn off when called with no arguments. (eglot--maybe-activate-editing-mode): Don't do anything if mode is already active. Suitable for calling from after-change-major-mode-hook. (after-change-major-mode-hook): Add eglot--maybe-activate-editing-mode. GitHub-reference: close https://github.com/joaotavora/eglot/issues/44 --- lisp/progmodes/eglot.el | 53 +++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8f9a43d93e8..49a1f3d496b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -244,7 +244,7 @@ Don't leave this function with the server still running." (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) + (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) ;; Now ask jsonrpc.el to shutdown server (which in normal ;; conditions should return immediately). (jsonrpc-shutdown server))) @@ -253,7 +253,7 @@ Don't leave this function with the server still running." "Called by jsonrpc.el when SERVER is already dead." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (with-current-buffer buffer (eglot--managed-mode-onoff server -1))) + (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) ;; Kill any expensive watches (maphash (lambda (_id watches) (mapcar #'file-notify-rm-watch watches)) @@ -691,11 +691,13 @@ If optional MARKERS, make markers." (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) + (add-hook 'kill-buffer-hook 'eglot--managed-mode-onoff nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) + (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu)) @@ -709,6 +711,7 @@ If optional MARKERS, make markers." (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) + (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) @@ -718,17 +721,22 @@ If optional MARKERS, make markers." "A cached reference to the current EGLOT server. Reset in `eglot--managed-mode-onoff'.") -(defun eglot--managed-mode-onoff (server arg) - "Proxy for function `eglot--managed-mode' with ARG and SERVER." - (eglot--managed-mode arg) +(defun eglot--managed-mode-onoff (&optional server turn-on) + "Proxy for function `eglot--managed-mode' with TURN-ON and SERVER." (let ((buf (current-buffer))) - (cond (eglot--managed-mode + (cond ((and server turn-on) + (eglot--managed-mode 1) (setq eglot--cached-current-server server) (cl-pushnew buf (eglot--managed-buffers server))) (t - (setq eglot--cached-current-server nil) - (setf (eglot--managed-buffers server) - (delq buf (eglot--managed-buffers server))))))) + (eglot--managed-mode -1) + (let ((server + (or server + eglot--cached-current-server))) + (setq eglot--cached-current-server nil) + (when server + (setf (eglot--managed-buffers server) + (delq buf (eglot--managed-buffers server))))))))) (add-hook 'eglot--managed-mode-hook 'flymake-mode) (add-hook 'eglot--managed-mode-hook 'eldoc-mode) @@ -754,21 +762,24 @@ Reset in `eglot--managed-mode-onoff'.") "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In that case, also signal textDocument/didOpen." - (unless server - (when eglot--cached-current-server - (display-warning - :eglot "`eglot--cached-current-server' is non-nil, but it should be!\n\ + (unless eglot--managed-mode + (unless server + (when eglot--cached-current-server + (display-warning + :eglot "`eglot--cached-current-server' is non-nil, but it shouldn't be!\n\ Please report this as a possible bug.") - (setq eglot--cached-current-server nil))) - ;; Called even when revert-buffer-in-progress-p - (let* ((cur (and buffer-file-name (eglot--current-server))) - (server (or (and (null server) cur) (and server (eq server cur) cur)))) - (when server - (setq eglot--unreported-diagnostics `(:just-opened . nil)) - (eglot--managed-mode-onoff server 1) - (eglot--signal-textDocument/didOpen)))) + (setq eglot--cached-current-server nil))) + ;; Called even when revert-buffer-in-progress-p + (let* ((cur (and buffer-file-name (eglot--current-server))) + (server (or (and (null server) cur) (and server (eq server cur) cur)))) + (when server + (setq eglot--unreported-diagnostics `(:just-opened . nil)) + (eglot--managed-mode-onoff server t) + (eglot--signal-textDocument/didOpen))))) + (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) +(add-hook 'after-change-major-mode-hook 'eglot--maybe-activate-editing-mode) (defun eglot-clear-status (server) "Clear the last JSONRPC error for SERVER." From 693e4282510f8ba8d70e6144707907ba9fbd56b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 22 Jul 2018 19:07:43 +0100 Subject: [PATCH 256/771] Don't turn on flymake-mode any more than is needed If flymake-mode is in eglot--managed-mode-hook, it will be called even if eglot--managed-mode is being turned off, which could be problematic because it triggers a check if flymake-start-on-flymake-mode is t. * eglot.el (eglot--managed-mode): Turn on flymake-mode and eldoc-mode here. GitHub-reference: close https://github.com/joaotavora/eglot/issues/44 --- lisp/progmodes/eglot.el | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 49a1f3d496b..cb9a5ed0515 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -700,7 +700,9 @@ If optional MARKERS, make markers." (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) - (add-function :around (local 'imenu-create-index-function) #'eglot-imenu)) + (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) + (flymake-mode 1) + (eldoc-mode 1)) (t (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) @@ -738,9 +740,6 @@ Reset in `eglot--managed-mode-onoff'.") (setf (eglot--managed-buffers server) (delq buf (eglot--managed-buffers server))))))))) -(add-hook 'eglot--managed-mode-hook 'flymake-mode) -(add-hook 'eglot--managed-mode-hook 'eldoc-mode) - (defun eglot--current-server () "Find the current logical EGLOT server." (or From eb7702b61de9f5683c5dfe66f77723f92d630433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 25 Jul 2018 18:19:13 +0000 Subject: [PATCH 257/771] Fix messages of eglot-ensure * eglot.el (eglot-ensure): fix messages. GitHub-reference: per https://github.com/joaotavora/eglot/issues/48 --- lisp/progmodes/eglot.el | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cb9a5ed0515..6a6a1b7f2bb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -417,13 +417,12 @@ INTERACTIVE is t if called interactively." (remove-hook 'post-command-hook #'maybe-connect nil) (eglot--with-live-buffer buffer (if eglot--managed-mode - (eglot--message "%s is already managed by existing `%s'" - buffer + (eglot--message "Buffer is already managed by existing `%s'" (eglot--project-nickname (eglot--current-server))) (let ((server (apply #'eglot--connect (eglot--guess-contact)))) (eglot--message "Automatically started `%s' to manage `%s' buffers in project `%s'" - (eglot--project-nickname server) + (jsonrpc-name server) major-mode (eglot--project-nickname server))))))) (when buffer-file-name From 273c5b62f3836a14e628eff0591b2601f81d736a Mon Sep 17 00:00:00 2001 From: Alan Zimmerman Date: Thu, 26 Jul 2018 12:32:24 +0200 Subject: [PATCH 258/771] Add entry for haskell-ide-engine in eglot-server-programs () * eglot.el (eglot-server-programs): Add entry for haskell-mode GitHub-reference: https://github.com/joaotavora/eglot/issues/49 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6a6a1b7f2bb..77f370e3471 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -89,7 +89,8 @@ . ("solargraph" "socket" "--port" :autoport)) (php-mode . ("php" "vendor/felixfbecker/\ -language-server/bin/php-language-server.php"))) +language-server/bin/php-language-server.php")) + (haskell-mode . ("hie-wrapper"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From 9bc459df6ddbd79f18fd569b008b5f82640c0fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 27 Jul 2018 10:06:21 +0100 Subject: [PATCH 259/771] Be less verbose when using eglot-ensure * eglot.el (eglot-ensure): Don't message when a buffer is already managed. GitHub-reference: close https://github.com/joaotavora/eglot/issues/48 --- lisp/progmodes/eglot.el | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 77f370e3471..695db498ff4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -137,7 +137,7 @@ lasted more than that many seconds." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) - + ;;; API (WORK-IN-PROGRESS!) ;;; (cl-defmacro eglot--with-live-buffer (buf &rest body) @@ -225,7 +225,7 @@ lasted more than that many seconds." :documentation "Represents a server. Wraps a process for LSP communication.") - + ;;; Process management (defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") @@ -417,9 +417,7 @@ INTERACTIVE is t if called interactively." () (remove-hook 'post-command-hook #'maybe-connect nil) (eglot--with-live-buffer buffer - (if eglot--managed-mode - (eglot--message "Buffer is already managed by existing `%s'" - (eglot--project-nickname (eglot--current-server))) + (unless eglot--managed-mode (let ((server (apply #'eglot--connect (eglot--guess-contact)))) (eglot--message "Automatically started `%s' to manage `%s' buffers in project `%s'" From eb279829cb5c11039abda16b92945a882cfcb39d Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sat, 28 Jul 2018 19:01:10 +0200 Subject: [PATCH 260/771] Work around emacs bugs 32237, 32278 () See: https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32237 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32278 * eglot.el (eglot--apply-text-edits): Inhibit modification hooks and call them manually for the changed region. GitHub-reference: https://github.com/joaotavora/eglot/issues/53 --- lisp/progmodes/eglot.el | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 695db498ff4..1ca7b6d65d1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1438,7 +1438,23 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (save-excursion (save-restriction (narrow-to-region beg end) - (replace-buffer-contents temp))) + + ;; On emacs versions < 26.2, + ;; `replace-buffer-contents' is buggy - it calls + ;; change functions with invalid arguments - so we + ;; manually call the change functions here. + ;; + ;; See emacs bugs #32237, #32278: + ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32237 + ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32278 + (let ((inhibit-modification-hooks t) + (length (- end beg))) + (run-hook-with-args 'before-change-functions + beg end) + (replace-buffer-contents temp) + (run-hook-with-args 'after-change-functions + beg (+ beg (length newText)) + length)))) (progress-reporter-update reporter (cl-incf done))))))) (mapcar (jsonrpc-lambda (&key range newText) (cons newText (eglot--range-region range 'markers))) From 6ffe90229b0a1503bb312880ef139d9ea311692b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=A4in=C3=B6=20J=C3=A4rvel=C3=A4?= Date: Sat, 28 Jul 2018 20:03:05 +0300 Subject: [PATCH 261/771] Fix typo in willsavewaituntil rpc request () Copyright-paperwork-exempt: yes * eglot.el (eglot--signal-textDocument/willSave): Fix typo. GitHub-reference: https://github.com/joaotavora/eglot/issues/51 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1ca7b6d65d1..3c01657abb7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1107,7 +1107,7 @@ When called interactively, use the currently active server" (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) (ignore-errors (eglot--apply-text-edits - (jsonrpc-request server :textDocument/willSaveWaituntil params + (jsonrpc-request server :textDocument/willSaveWaitUntil params :timeout 0.5)))))) (defun eglot--signal-textDocument/didSave () From e6a801ccf6f745d149306d2afaaa4b3d313415e9 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sat, 28 Jul 2018 19:14:02 +0200 Subject: [PATCH 262/771] Correctly make lsp positions in narrowed buffers * eglot.el (eglot--pos-to-lsp-position): Fix return value when narrowing is in effect. GitHub-reference: close https://github.com/joaotavora/eglot/issues/54 --- lisp/progmodes/eglot.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3c01657abb7..fbc6a53db5e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -597,10 +597,10 @@ CONNECT-ARGS are passed as additional arguments to (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." - (save-excursion - (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE - :character (- (goto-char (or pos (point))) - (line-beginning-position))))) + (eglot--widening + (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE + :character (- (goto-char (or pos (point))) + (line-beginning-position))))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. From 5fc7c9a9ef51349375a73e6640a16a75f3ffbf60 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Tue, 10 Jul 2018 15:10:25 +0200 Subject: [PATCH 263/771] Implement textdocument/rangeformatting * eglot.el (eglot-format): New command. (eglot-format-buffer): Use it as implementation. (eglot-client-capabilities): Add :rangeFormatting. * eglot-tests.el (formatting): Also test range formatting. * README.md (Commands and keybindings): Mention eglot-format. (Language features): Tick textDocument/rangeFormatting. --- lisp/progmodes/eglot.el | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fbc6a53db5e..f0a4b7144dc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -183,6 +183,7 @@ lasted more than that many seconds." :documentHighlight `(:dynamicRegistration :json-false) :codeAction `(:dynamicRegistration :json-false) :formatting `(:dynamicRegistration :json-false) + :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) :experimental (list)))) @@ -1227,17 +1228,35 @@ DUMMY is ignored." (defun eglot-format-buffer () "Format contents of current buffer." (interactive) - (unless (eglot--server-capable :documentFormattingProvider) - (eglot--error "Server can't format!")) - (eglot--apply-text-edits - (jsonrpc-request - (eglot--current-server-or-lose) - :textDocument/formatting - (list :textDocument (eglot--TextDocumentIdentifier) - :options (list :tabSize tab-width - :insertSpaces - (if indent-tabs-mode :json-false t))) - :deferred :textDocument/formatting))) + (eglot-format nil nil)) + +(defun eglot-format (&optional beg end) + "Format region BEG END. +If either BEG or END is nil, format entire buffer. +Interactively, format active region, or entire buffer if region +is not active." + (interactive (and (region-active-p) (list (region-beginning) (region-end)))) + (pcase-let ((`(,method ,cap ,args) + (cond + ((and beg end) + `(:textDocument/rangeFormatting + :documentRangeFormattingProvider + (:range ,(list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end))))) + (t + '(:textDocument/formatting :documentFormattingProvider nil))))) + (unless (eglot--server-capable cap) + (eglot--error "Server can't format!")) + (eglot--apply-text-edits + (jsonrpc-request + (eglot--current-server-or-lose) + method + (cl-list* + :textDocument (eglot--TextDocumentIdentifier) + :options (list :tabSize tab-width + :insertSpaces (if indent-tabs-mode :json-false t)) + args) + :deferred method)))) (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." From 308b1a9ee3bce89512d743e4a58116aa984310f0 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Mon, 30 Jul 2018 22:17:32 +0200 Subject: [PATCH 264/771] * eglot.el (eglot-client-capabilities): fix a typo. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f0a4b7144dc..f5903138100 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -167,7 +167,7 @@ lasted more than that many seconds." :applyEdit t :executeCommand `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges :json-false) - :didChangeWatchesFiles `(:dynamicRegistration t) + :didChangeWatchedFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false)) :textDocument (list From b14cba5cb73dfaa94b4f277362c938bf83a48b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 2 Aug 2018 11:02:20 +0100 Subject: [PATCH 265/771] Erase company-doc buffer in between doc requests * eglot.el (eglot-completion-at-point): Erase temporary "*eglot-doc*" buffer for company's doc. GitHub-reference: close https://github.com/joaotavora/eglot/issues/58 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f5903138100..0bb162b07c9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1312,6 +1312,7 @@ is not active." :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") + (erase-buffer) (insert (eglot--format-markup documentation)) (current-buffer))))) :exit-function (lambda (_string _status) From 7b7312f8d66ca7a34fb509704f77009db7d7aaa7 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Fri, 3 Aug 2018 10:22:18 +0200 Subject: [PATCH 266/771] Fix placement of diagnostics with same start and end positions Some servers such as cquery and clangd publish diagnostic with identical start and end positions. * eglot.el (eglot-handle-notification :textDocument/publishDiagnostics): Add 1 to :line since LSP lines are 0-based. Don't subtract 1 from :character, since both emacs and LSP have 0-based columns. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0bb162b07c9..58efc166863 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -929,8 +929,8 @@ Uses THING, FACE, DEFS and PREPEND." (let* ((st (plist-get range :start)) (diag-region (flymake-diag-region - (current-buffer) (plist-get st :line) - (1- (plist-get st :character))))) + (current-buffer) (1+ (plist-get st :line)) + (plist-get st :character)))) (setq beg (car diag-region) end (cdr diag-region)))) (eglot--make-diag (current-buffer) beg end From af0e2e5b1fea4dbf21c556ca1245d7623cc92250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 3 Aug 2018 09:14:42 +0100 Subject: [PATCH 267/771] Default eglot-handle-notifictiona|request must &allow-other-keys * eglot.el (eglot-handle-notification, eglot-handle-request): Add &allow-other-keys --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58efc166863..7e825d6746a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -872,12 +872,12 @@ Uses THING, FACE, DEFS and PREPEND." ;;; Protocol implementation (Requests, notifications, etc) ;;; (cl-defmethod eglot-handle-notification - (_server method &key) + (_server method &key &allow-other-keys) "Handle unknown notification" (eglot--warn "Server sent unknown notification method `%s'" method)) (cl-defmethod eglot-handle-request - (_server method &key) + (_server method &key &allow-other-keys) "Handle unknown request" (jsonrpc-error "Unknown request method `%s'" method)) From ea04e60ce5a9b8ca158668d7611331ab7713f0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 6 Aug 2018 01:10:12 +0100 Subject: [PATCH 268/771] Eglot-workspace-configuration's keys needn't be keywords * eglot.el (eglot-signal-didChangeConfiguration): Convert alist keys into a json-compatible plist. GitHub-reference: per https://github.com/joaotavora/eglot/issues/59 --- lisp/progmodes/eglot.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7e825d6746a..c3a0d518d85 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1062,7 +1062,10 @@ When called interactively, use the currently active server" (list :settings (cl-loop for (k . v) in eglot-workspace-configuration - collect k collect v)))) + collect (if (keywordp k) + k + (intern (format ":%s" k))) + collect v)))) (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." From 71a3fb813f8e4055bc4f9f68db73be5aa17d0867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 6 Aug 2018 17:53:09 +0100 Subject: [PATCH 269/771] Accept functions as entries in eglot-server-programs CONTACT in the (MAJOR-MODE . CONTACT) association in eglot-server-programs can now be a function of no arguments producing any value previously valid for contact. The function is called at time of `M-x eglot` or `eglot-ensure`. This is useful for servers requiring command-line invocations that depend on the specific momentary environment. * eglot.el (eglot-server-programs): CONTACT can be a fucntion of no arguments. (eglot--guess-contact, eglot--connect): Accept function CONTACTs. GitHub-reference: per https://github.com/joaotavora/eglot/issues/63 --- lisp/progmodes/eglot.el | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c3a0d518d85..33b478f4af6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -121,7 +121,10 @@ of those modes. CONTACT can be: converted to produce a plist with a suitable :PROCESS initarg to CLASS-NAME. The class `eglot-lsp-server' descends `jsonrpc-process-connection', which you should see for the - semantics of the mandatory :PROCESS argument.") + semantics of the mandatory :PROCESS argument. + +* A function of no arguments producing any of the above values + for CONTACT.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -310,6 +313,7 @@ be guessed." (lambda (m1 m2) (or (eq m1 m2) (and (listp m1) (memq m2 m1))))))) + (guess (if (functionp guess) (funcall guess) guess)) (class (or (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) @@ -457,6 +461,7 @@ This docstring appeases checkdoc, that's all." (nickname (file-name-base (directory-file-name default-directory))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) autostart-inferior-process + (contact (if (functionp contact) (funcall contact) contact)) (initargs (cond ((keywordp (car contact)) contact) ((integerp (cadr contact)) @@ -1658,7 +1663,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Passes through required cquery initialization options" (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) - (list :cacheDirectory (file-name-as-directory cache) + (list :cacheDirectory (file-name-as-directory cache)Ini :progressReportFrequencyMs -1))) From c93150ebbe929eef79e023bce4463d24ae245a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 7 Aug 2018 22:13:28 +0100 Subject: [PATCH 270/771] * eglot.el (eglot-initialization-options): fix spurious typo. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 33b478f4af6..125c795bc35 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1663,7 +1663,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Passes through required cquery initialization options" (let* ((root (car (project-roots (eglot--project server)))) (cache (expand-file-name ".cquery_cached_index/" root))) - (list :cacheDirectory (file-name-as-directory cache)Ini + (list :cacheDirectory (file-name-as-directory cache) :progressReportFrequencyMs -1))) From da11bba15e8026cd177364d3be9d87554b3ab147 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Thu, 9 Aug 2018 21:21:32 +0200 Subject: [PATCH 271/771] Notify server of recent changes before save notification * eglot.el (eglot--signal-textDocument/didSave): Call eglot--signal-textDocument/didChange. GitHub-reference: close https://github.com/joaotavora/eglot/issues/60 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 125c795bc35..d29bef540cb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1121,6 +1121,7 @@ When called interactively, use the currently active server" (defun eglot--signal-textDocument/didSave () "Send textDocument/didSave to server." + (eglot--signal-textDocument/didChange) (jsonrpc-notify (eglot--current-server-or-lose) :textDocument/didSave From c61b3624f5a20efaea3ff997d48100917c286ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 10 Aug 2018 01:03:59 +0100 Subject: [PATCH 272/771] Snappier completions that don't hinder typing This should improve company-capf's performance. * eglot.el (Package-Requires): Require jsonrpc 1.0,1 (eglot-completion-at-point): Use completion-table-dynamic. Pass CANCEL-ON-INPUT to jsonrpc-request. GitHub-reference: close https://github.com/joaotavora/eglot/issues/61 --- lisp/progmodes/eglot.el | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d29bef540cb..9e714163a47 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.1")) ;; 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 @@ -1275,12 +1275,13 @@ is not active." (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (completion-table-with-cache + (completion-table-dynamic (lambda (_ignored) (let* ((resp (jsonrpc-request server :textDocument/completion (eglot--TextDocumentPositionParams) - :deferred :textDocument/completion)) + :deferred :textDocument/completion + :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) @@ -1317,7 +1318,8 @@ is not active." (plist-get (jsonrpc-request server :completionItem/resolve (get-text-property - 0 'eglot--lsp-completion obj)) + 0 'eglot--lsp-completion obj) + :cancel-on-input t) :documentation))))) (when documentation (with-current-buffer (get-buffer-create " *eglot doc*") From 0eddf00dd9953c680b739ce14ff1dc3bd9ad8e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 10 Aug 2018 01:28:41 +0100 Subject: [PATCH 273/771] Require jsonrpc.el 1.0.2 (gnu elpa didn't build 1.0.1) * eglot.el (Package-Requires): Require jsonrpc 1.0.2 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9e714163a47..3b1b364ac96 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.1")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.2")) ;; 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 From 4144d9adc5cea0fb8c2bb9db217cc8fd2d625e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 10 Aug 2018 01:42:01 +0100 Subject: [PATCH 274/771] * eglot.el (advice-add jsonrpc-request): add &allow-other-keys --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3b1b364ac96..7f0e91dbbc2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1048,7 +1048,8 @@ Records START, END and PRE-CHANGE-LENGTH locally." ;; bad idea, since that might lead to the request never having a ;; chance to run, because `jsonrpc-connection-ready-p'. (advice-add #'jsonrpc-request :before - (cl-function (lambda (_proc _method _params &key deferred _timeout) + (cl-function (lambda (_proc _method _params &key + deferred &allow-other-keys) (when (and eglot--managed-mode deferred) (eglot--signal-textDocument/didChange)))) '((name . eglot--signal-textDocument/didChange))) From c76c0240dcb4cb1b11f58852c598986f3328d2d7 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Fri, 10 Aug 2018 19:13:59 +0200 Subject: [PATCH 275/771] * eglot.el (eglot-cquery): capitalize docstring. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7f0e91dbbc2..9e16d4316e4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1661,7 +1661,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ;;; cquery-specific ;;; (defclass eglot-cquery (eglot-lsp-server) () - :documentation "cquery's C/C++ langserver.") + :documentation "Cquery's C/C++ langserver.") (cl-defmethod eglot-initialization-options ((server eglot-cquery)) "Passes through required cquery initialization options" From 2ebf34f1e12af56c4c2ff9bbaa0b01f4827b5005 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Thu, 9 Aug 2018 15:24:29 +0200 Subject: [PATCH 276/771] Add a generic eglot-execute-command api * eglot.el (eglot-execute-command): New defgeneric and method. Use it to request :workspace/executeCommand. (eglot-code-actions): Use it. --- lisp/progmodes/eglot.el | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9e16d4316e4..e250ce09243 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -158,6 +158,9 @@ lasted more than that many seconds." (cl-defgeneric eglot-handle-notification (server method id &rest params) "Handle SERVER's METHOD notification with PARAMS.") +(cl-defgeneric eglot-execute-command (server command arguments) + "Execute on SERVER COMMAND with ARGUMENTS.") + (cl-defgeneric eglot-initialization-options (server) "JSON object to send under `initializationOptions'" (:method (_s) nil)) ; blank default @@ -886,6 +889,14 @@ Uses THING, FACE, DEFS and PREPEND." "Handle unknown request" (jsonrpc-error "Unknown request method `%s'" method)) +(cl-defmethod eglot-execute-command + (server command arguments) + "Execute command by making a :workspace/executeCommand request." + (jsonrpc-request + server + :workspace/executeCommand + `(:command ,command :arguments ,arguments))) + (cl-defmethod eglot-handle-notification (_server (_method (eql window/showMessage)) &key type message) "Handle notification window/showMessage" @@ -1576,7 +1587,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (keyboard-quit) retval)))))) (if command-and-args - (jsonrpc-request server :workspace/executeCommand command-and-args) + (eglot-execute-command server (plist-get command-and-args :command) + (plist-get command-and-args :arguments)) (eglot--message "No code actions here")))) From 4dc3c8d0dce1ebf0d26d9092593bddb4119c26d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 11 Aug 2018 17:28:59 +0100 Subject: [PATCH 277/771] Improve eglot-execute-command api to ease overriding by servers * eglot.el (eglot-execute-command): COMMAND can be a symbol. (eglot-code-actions): Pass symbols to eglot-command. --- lisp/progmodes/eglot.el | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e250ce09243..e37ec94ab0a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -159,7 +159,7 @@ lasted more than that many seconds." "Handle SERVER's METHOD notification with PARAMS.") (cl-defgeneric eglot-execute-command (server command arguments) - "Execute on SERVER COMMAND with ARGUMENTS.") + "Ask SERVER to execute COMMAND with ARGUMENTS.") (cl-defgeneric eglot-initialization-options (server) "JSON object to send under `initializationOptions'" @@ -891,11 +891,10 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defmethod eglot-execute-command (server command arguments) - "Execute command by making a :workspace/executeCommand request." - (jsonrpc-request - server - :workspace/executeCommand - `(:command ,command :arguments ,arguments))) + "Execute COMMAND on SERVER with `:workspace/executeCommand'. +COMMAND is a symbol naming the command." + (jsonrpc-request server :workspace/executeCommand + `(:command ,(format "%s" command) :arguments ,arguments))) (cl-defmethod eglot-handle-notification (_server (_method (eql window/showMessage)) &key type message) @@ -1586,10 +1585,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eq (setq retval (tmm-prompt menu)) never-mind) (keyboard-quit) retval)))))) - (if command-and-args - (eglot-execute-command server (plist-get command-and-args :command) - (plist-get command-and-args :arguments)) - (eglot--message "No code actions here")))) + (cl-destructuring-bind (&key _title command arguments) command-and-args + (if command + (eglot-execute-command server (intern command) arguments) + (eglot--message "No code actions here"))))) From ef5266397a0447a356b401df5572becdb30e7c61 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sun, 12 Aug 2018 01:22:26 +0200 Subject: [PATCH 278/771] Kill server's output and events buffers from eglot-shutdown () * eglot.el (Package-Requires): Require jsonrpc 1.0.5 (eglot-shutdown): Kill events and stderr buffers of the server, unless new PRESERVE-BUFFERS argument is non-nil. eglot-reconnect): Preserve buffers on shutdown. * eglot-tests.el (eglot--call-with-dirs-and-files): Call eglot-shutdown with non-nil PRESERVE-BUFFERS arg. GitHub-reference: https://github.com/joaotavora/eglot/issues/66 --- lisp/progmodes/eglot.el | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e37ec94ab0a..70a725c22f1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.2")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.5")) ;; 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 @@ -237,11 +237,14 @@ lasted more than that many seconds." (defvar eglot--servers-by-project (make-hash-table :test #'equal) "Keys are projects. Values are lists of processes.") -(defun eglot-shutdown (server &optional _interactive timeout) +(defun eglot-shutdown (server &optional _interactive timeout preserve-buffers) "Politely ask SERVER to quit. Forcefully quit it if it doesn't respond within TIMEOUT seconds. -Don't leave this function with the server still running." - (interactive (list (eglot--current-server-or-lose) t)) +If PRESERVE-BUFFERS is non-nil (interactively, when called with a +prefix argument), do not kill events and output buffers of +SERVER. Don't leave this function with the server still +running." + (interactive (list (eglot--current-server-or-lose) t nil current-prefix-arg)) (eglot--message "Asking %s politely to terminate" (jsonrpc-name server)) (unwind-protect (progn @@ -253,9 +256,12 @@ Don't leave this function with the server still running." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) - ;; Now ask jsonrpc.el to shutdown server (which in normal + ;; Now ask jsonrpc.el to shut down the server (which under normal ;; conditions should return immediately). - (jsonrpc-shutdown server))) + (jsonrpc-shutdown server (not preserve-buffers)) + (unless preserve-buffers + (mapc #'kill-buffer + `(,(jsonrpc-events-buffer server) ,(jsonrpc-stderr-buffer server)))))) (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." @@ -408,7 +414,7 @@ managing `%s' buffers in project `%s'." INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose) t)) (when (jsonrpc-running-p server) - (ignore-errors (eglot-shutdown server interactive))) + (ignore-errors (eglot-shutdown server interactive nil 'preserve-buffers))) (eglot--connect (eglot--major-mode server) (eglot--project server) (eieio-object-class-name server) From d164ece5cf0fefec4c36c1b80f9f43c35e00f4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 11 Aug 2018 14:52:33 +0100 Subject: [PATCH 279/771] Implement asynchronous server connection A new defcustom eglot-sync-connect controls this feature. If it is t, eglot should behave like previously, waiting synchronously for a connection to be established, with the exception that there is now a non-nil timeout set to eglot-connect-timeout, which defaults to 30 seconds. eglot-connect is now considerably more complicated as it replicates most of the work that jsonrpc-request does vis-a-vis handling errors, timeouts and user quits.. * eglot-tests.el (eglot--call-with-dirs-and-files): Simplify cleanup logic. (slow-sync-connection-wait) (slow-sync-connection-intime, slow-async-connection) (slow-sync-error): New tests. * eglot.el (eglot-sync-connect): New defcustom. (eglot-ensure, eglot): Simplify. (eglot--connect): Honour eglot-sync-connect. Complicate considerably. (eglot-connect-timeout): New defcustom. (Package-requires): Require jsonrpc 1.0.6 GitHub-reference: close https://github.com/joaotavora/eglot/issues/68 --- lisp/progmodes/eglot.el | 138 ++++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 49 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 70a725c22f1..ac529dc8d2f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.5")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.6")) ;; 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 @@ -140,6 +140,19 @@ lasted more than that many seconds." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) +(defcustom eglot-connect-timeout 30 + "Number of seconds before timing out LSP connection attempts. +If nil, never time out." + :type 'number) + +(defcustom eglot-sync-connect 3 + "Control blocking of LSP connection attempts. +If t, block for `eglot-connect-timeout' seconds. A positive +integer number means block for that many seconds, and then wait +for the connection in the background. nil has the same meaning +as 0, i.e. don't block at all." + :type '(choice (boolean :tag "Whether to inhibit autoreconnection") + (integer :tag "Number of seconds"))) ;;; API (WORK-IN-PROGRESS!) ;;; @@ -259,9 +272,7 @@ running." ;; Now ask jsonrpc.el to shut down the server (which under normal ;; conditions should return immediately). (jsonrpc-shutdown server (not preserve-buffers)) - (unless preserve-buffers - (mapc #'kill-buffer - `(,(jsonrpc-events-buffer server) ,(jsonrpc-stderr-buffer server)))))) + (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." @@ -399,15 +410,7 @@ INTERACTIVE is t if called interactively." (y-or-n-p "[eglot] Live process found, reconnect instead? ")) (eglot-reconnect current-server interactive) (when live-p (ignore-errors (eglot-shutdown current-server))) - (let ((server (eglot--connect managed-major-mode - project - class - contact))) - (eglot--message "Connected! Process `%s' now \ -managing `%s' buffers in project `%s'." - (jsonrpc-name server) managed-major-mode - (eglot--project-nickname server)) - server)))) + (eglot--connect managed-major-mode project class contact)))) (defun eglot-reconnect (server &optional interactive) "Reconnect to SERVER. @@ -432,12 +435,7 @@ INTERACTIVE is t if called interactively." (remove-hook 'post-command-hook #'maybe-connect nil) (eglot--with-live-buffer buffer (unless eglot--managed-mode - (let ((server (apply #'eglot--connect (eglot--guess-contact)))) - (eglot--message - "Automatically started `%s' to manage `%s' buffers in project `%s'" - (jsonrpc-name server) - major-mode - (eglot--project-nickname server))))))) + (apply #'eglot--connect (eglot--guess-contact)))))) (when buffer-file-name (add-hook 'post-command-hook #'maybe-connect 'append nil))))) @@ -508,42 +506,84 @@ This docstring appeases checkdoc, that's all." :request-dispatcher (funcall spread #'eglot-handle-request) :on-shutdown #'eglot--on-shutdown initargs)) - success) + (cancelled nil) + (tag (make-symbol "connected-catch-tag"))) (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) (setf (eglot--inferior-process server) autostart-inferior-process) - (push server (gethash project eglot--servers-by-project)) - (run-hook-with-args 'eglot-connect-hook server) + ;; Now start the handshake. To honour `eglot-sync-connect' + ;; maybe-sync-maybe-async semantics we use `jsonrpc-async-request' + ;; and mimic most of `jsonrpc-request'. (unwind-protect - (cl-destructuring-bind (&key capabilities) - (jsonrpc-request - server - :initialize - (list :processId (unless (eq (jsonrpc-process-type server) 'network) - (emacs-pid)) - :rootPath (expand-file-name default-directory) - :rootUri (eglot--path-to-uri default-directory) - :initializationOptions (eglot-initialization-options server) - :capabilities (eglot-client-capabilities server))) - (setf (eglot--capabilities server) capabilities) - (dolist (buffer (buffer-list)) - (with-current-buffer buffer - (eglot--maybe-activate-editing-mode server))) - (jsonrpc-notify server :initialized `(:__dummy__ t)) - (run-hook-with-args 'eglot-server-initialized-hook server) - (setf (eglot--inhibit-autoreconnect server) - (cond - ((booleanp eglot-autoreconnect) (not eglot-autoreconnect)) - ((cl-plusp eglot-autoreconnect) - (run-with-timer eglot-autoreconnect nil - (lambda () - (setf (eglot--inhibit-autoreconnect server) - (null eglot-autoreconnect))))))) - (setq success server)) - (when (and (not success) (jsonrpc-running-p server)) - (eglot-shutdown server))))) + (condition-case _quit + (let ((retval + (catch tag + (jsonrpc-async-request + server + :initialize + (list :processId (unless (eq (jsonrpc-process-type server) + 'network) + (emacs-pid)) + :rootPath (expand-file-name default-directory) + :rootUri (eglot--path-to-uri default-directory) + :initializationOptions (eglot-initialization-options + server) + :capabilities (eglot-client-capabilities server)) + :success-fn + (jsonrpc-lambda (&key capabilities) + (unless cancelled + (push server + (gethash project eglot--servers-by-project)) + (setf (eglot--capabilities server) capabilities) + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (eglot--maybe-activate-editing-mode server))) + (jsonrpc-notify server :initialized `(:__dummy__ t)) + (setf (eglot--inhibit-autoreconnect server) + (cond + ((booleanp eglot-autoreconnect) + (not eglot-autoreconnect)) + ((cl-plusp eglot-autoreconnect) + (run-with-timer + eglot-autoreconnect nil + (lambda () + (setf (eglot--inhibit-autoreconnect server) + (null eglot-autoreconnect))))))) + (run-hook-with-args 'eglot-connect-hook server) + (run-hook-with-args 'eglot-server-initialized-hook server) + (eglot--message + "Connected! Server `%s' now managing `%s' buffers \ +in project `%s'." + (jsonrpc-name server) managed-major-mode + (eglot--project-nickname server)) + (when tag (throw tag t)))) + :timeout eglot-connect-timeout + :error-fn (jsonrpc-lambda (&key code message _data) + (unless cancelled + (jsonrpc-shutdown server) + (let ((msg (format "%s: %s" code message))) + (if tag (throw tag `(error . ,msg)) + (eglot--error msg))))) + :timeout-fn (lambda () + (unless cancelled + (jsonrpc-shutdown server) + (let ((msg (format "Timed out"))) + (if tag (throw tag `(error . ,msg)) + (eglot--error msg)))))) + (cond ((numberp eglot-sync-connect) + (accept-process-output nil eglot-sync-connect)) + (eglot-sync-connect + (while t (accept-process-output nil 30))))))) + (pcase retval + (`(error . ,msg) (eglot--error msg)) + (`nil (eglot--message "Waiting in background for server `%s'" + (jsonrpc-name server)) + nil) + (_ server))) + (quit (jsonrpc-shutdown server) (setq cancelled 'quit))) + (setq tag nil)))) (defun eglot--inferior-bootstrap (name contact &optional connect-args) "Use CONTACT to start a server, then connect to it. From cc2044834e2e15d76db630446af88ffe45fd50bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 13 Aug 2018 01:29:41 +0100 Subject: [PATCH 280/771] Control the size of the events buffer * eglot.el (eglot-events-buffer-size): New defcustom. (eglot--connect): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/41 --- lisp/progmodes/eglot.el | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ac529dc8d2f..eeea104e04c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -154,6 +154,14 @@ as 0, i.e. don't block at all." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) +(defcustom eglot-events-buffer-size 2000000 + "Control the size of the Eglot events buffer. +If a number, don't let the buffer grow larger than that many +characters. If 0, don't use an event's buffer at all. If nil, +let the buffer grow forever." + :type '(choice (const :tag "No limit" nil) + (integer :tag "Number of characters"))) + ;;; API (WORK-IN-PROGRESS!) ;;; (cl-defmacro eglot--with-live-buffer (buf &rest body) @@ -502,6 +510,7 @@ This docstring appeases checkdoc, that's all." (apply #'make-instance class :name readable-name + :events-buffer-scrollback-size eglot-events-buffer-size :notification-dispatcher (funcall spread #'eglot-handle-notification) :request-dispatcher (funcall spread #'eglot-handle-request) :on-shutdown #'eglot--on-shutdown From b5e28c2ea6434d6216d927bac169bbdb089da969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 13 Aug 2018 01:45:40 +0100 Subject: [PATCH 281/771] Handle edits to same position in the correct order In eglot--apply-text-edits, the markers returned by eglot--lsp-position-to-point are of the "stay" type, i.e. have an insertion-type of nil. This causes multiple insertion edits to the same location to happen in the reverse order in which they appear in the LSP message, which is a violation of the spec and a bug. There are more ways to solve this (see related discuttion in https://github.com/joaotavora/eglot/pull/64), but the easiest way is to revert the order in which the edits are processed. This is because the spec tells us that the order is only relevant in precisely this "same position" case. So if we reverse the order we fix this bug and don't break anything else. * eglot.el (eglot--apply-text-edits): Apply edits in reverse.. GitHub-reference: close https://github.com/joaotavora/eglot/issues/64 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eeea104e04c..427fa293b68 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1555,7 +1555,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (progress-reporter-update reporter (cl-incf done))))))) (mapcar (jsonrpc-lambda (&key range newText) (cons newText (eglot--range-region range 'markers))) - edits)) + (reverse edits))) (undo-amalgamate-change-group change-group) (progress-reporter-done reporter)))) From 04415fa0665e097d4dd4eeabc23e19047986ff91 Mon Sep 17 00:00:00 2001 From: James Nguyen Date: Sun, 12 Aug 2018 17:50:17 -0700 Subject: [PATCH 282/771] Add kotlin-language-server () https://github.com/fwcd/KotlinLanguageServer copyright-paperwork-exempt: yes * README.md (Installation and Usage): declare Kotlin support. GitHub-reference: https://github.com/joaotavora/eglot/issues/70 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 427fa293b68..43334adffa6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -90,7 +90,8 @@ :autoport)) (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) - (haskell-mode . ("hie-wrapper"))) + (haskell-mode . ("hie-wrapper")) + (kotlin-mode . ("kotlin-language-server"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From b72a4e4e2e241703b74a91ff3157c366bd07bbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 13 Aug 2018 20:02:48 +0100 Subject: [PATCH 283/771] Prompt for server in interactive eglot-shutdown * eglot.el (eglot--read-server): New helper. (eglot-shutdown): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/73 --- lisp/progmodes/eglot.el | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 43334adffa6..88c6b45141a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -261,12 +261,18 @@ let the buffer grow forever." (defun eglot-shutdown (server &optional _interactive timeout preserve-buffers) "Politely ask SERVER to quit. +Interactively, read SERVER from the minibuffer unless there is +only one and it's managing the current buffer. + Forcefully quit it if it doesn't respond within TIMEOUT seconds. +Don't leave this function with the server still running. + If PRESERVE-BUFFERS is non-nil (interactively, when called with a prefix argument), do not kill events and output buffers of -SERVER. Don't leave this function with the server still -running." - (interactive (list (eglot--current-server-or-lose) t nil current-prefix-arg)) +SERVER. ." + (interactive (list (eglot--read-server "Shutdown which server" + (eglot--current-server)) + t nil current-prefix-arg)) (eglot--message "Asking %s politely to terminate" (jsonrpc-name server)) (unwind-protect (progn @@ -737,6 +743,32 @@ If optional MARKERS, make markers." (end (eglot--lsp-position-to-point (plist-get range :end) markers))) (cons beg end))) +(defun eglot--read-server (prompt &optional dont-if-just-the-one) + "Read a running Eglot server from minibuffer using PROMPT. +If DONT-IF-JUST-THE-ONE and there's only one server, don't prompt +and just return it. PROMPT shouldn't end with a question mark." + (let ((servers (cl-loop for servers + being hash-values of eglot--servers-by-project + append servers)) + (name (lambda (srv) + (format "%s/%s" (eglot--project-nickname srv) + (eglot--major-mode srv))))) + (cond ((null servers) + (eglot--error "No servers!")) + ((or (cdr servers) (not dont-if-just-the-one)) + (let* ((default (when-let ((current (eglot--current-server))) + (funcall name current))) + (read (completing-read + (if default + (format "%s (default %s)? " prompt default) + (concat prompt "? ")) + (mapcar name servers) + nil t + nil nil + default))) + (cl-find read servers :key name :test #'equal))) + (t (car servers))))) + ;;; Minor modes ;;; From 25a7b3d4a1ab26d96144155b06cde1052cebb4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 17 Aug 2018 23:23:31 +0100 Subject: [PATCH 284/771] Fix eglot-capabilities when querying for multiple features * eglot-tests.el (eglot-capabilities): New test. * eglot.el (eglot--server-capable): Fix problems with queries for multiple capabilities. GitHub-reference: per https://github.com/joaotavora/eglot/issues/74 --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 88c6b45141a..63fbce205d5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -728,12 +728,12 @@ under cursor." feats) (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) then (cadr probe) - for feat in feats + for (feat . more) on feats for probe = (plist-member caps feat) if (not probe) do (cl-return nil) if (eq (cadr probe) :json-false) do (cl-return nil) - if (not (listp (cadr probe))) do (cl-return (cadr probe)) - finally (cl-return (or probe t))))) + if (not (listp (cadr probe))) do (cl-return (if more nil (cadr probe))) + finally (cl-return (or (cadr probe) t))))) (defun eglot--range-region (range &optional markers) "Return region (BEG . END) that represents LSP RANGE. From 0f33ef3e67cc16b04c7308688788d2c59b144a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 10 Aug 2018 02:29:26 +0100 Subject: [PATCH 285/771] Support snippet completions * eglot.el (eglot-client-capabilities): Declare support for snippet-based completions. (eglot-completion-at-point): Expand snippet completions with YASnippet if that is found. (eglot-note, eglot-warning, eglot-error): Diagnostic overlay priorities have to be slightly lower than yasnippet's, which must be reasonably high. GitHub-reference: close https://github.com/joaotavora/eglot/issues/50 --- lisp/progmodes/eglot.el | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 63fbce205d5..13c72abdc4e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -202,7 +202,8 @@ let the buffer grow forever." :synchronization (list :dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t) - :completion `(:dynamicRegistration :json-false) + :completion (list :dynamicRegistration :json-false + :completionItem `(:snippetSupport t)) :hover `(:dynamicRegistration :json-false) :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) @@ -956,13 +957,15 @@ Uses THING, FACE, DEFS and PREPEND." (defalias 'eglot--make-diag 'flymake-make-diagnostic) (defalias 'eglot--diag-data 'flymake-diagnostic-data) -(dolist (type '(eglot-error eglot-warning eglot-note)) - (put type 'flymake-overlay-control - `((mouse-face . highlight) - (keymap . ,(let ((map (make-sparse-keymap))) - (define-key map [mouse-1] - (eglot--mouse-call 'eglot-code-actions)) - map))))) +(cl-loop for i from 1 + for type in '(eglot-note eglot-warning eglot-error ) + do (put type 'flymake-overlay-control + `((mouse-face . highlight) + (priority . ,(+ 50 i)) + (keymap . ,(let ((map (make-sparse-keymap))) + (define-key map [mouse-1] + (eglot--mouse-call 'eglot-code-actions)) + map))))) ;;; Protocol implementation (Requests, notifications, etc) @@ -1384,7 +1387,7 @@ is not active." (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText label))) + (let ((insert (or insertText (string-trim-left label)))) (add-text-properties 0 1 all insert) (put-text-property 0 1 'eglot--lsp-completion all insert) insert)) @@ -1425,9 +1428,17 @@ is not active." (erase-buffer) (insert (eglot--format-markup documentation)) (current-buffer))))) - :exit-function (lambda (_string _status) - (eglot--signal-textDocument/didChange) - (eglot-eldoc-function)))))) + :exit-function (lambda (obj _status) + (cl-destructuring-bind (&key insertTextFormat + insertText + &allow-other-keys) + (text-properties-at 0 obj) + (when (and (eql insertTextFormat 2) + (fboundp 'yas-expand-snippet)) + (delete-region (- (point) (length obj)) (point)) + (funcall 'yas-expand-snippet insertText)) + (eglot--signal-textDocument/didChange) + (eglot-eldoc-function))))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") From 7704fbac0be3bc40ec99467cbe5ed04f80698335 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sat, 18 Aug 2018 01:56:01 +0200 Subject: [PATCH 286/771] Fix textdocument/hover responses where markedstring is a plist () * eglot.el (eglot--hover-info): Forward all non-vector content to eglot--format-markup. GitHub-reference: https://github.com/joaotavora/eglot/issues/72 --- lisp/progmodes/eglot.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13c72abdc4e..6f3676117a7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1446,8 +1446,7 @@ is not active." (let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (concat (buffer-substring beg end) ": ")))) (body (mapconcat #'eglot--format-markup - (append (cond ((vectorp contents) contents) - ((stringp contents) (list contents)))) "\n"))) + (if (vectorp contents) contents (list contents)) "\n"))) (when (or heading (cl-plusp (length body))) (concat heading body)))) (defun eglot--sig-info (sigs active-sig active-param) From 88e9d97119dbb8879b74634b88d1c8e7da2bb5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 18 Aug 2018 12:26:55 +0100 Subject: [PATCH 287/771] Don't error if server replies with empty hover message * eglot.el (eglot-eldoc-function): Check non-nil contents. GitHub-reference: per https://github.com/joaotavora/eglot/issues/74 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6f3676117a7..3fd98c7d800 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1512,7 +1512,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :success-fn (jsonrpc-lambda (&key contents range) (unless sig-showing (when-buffer-window - (when-let (info (eglot--hover-info contents range)) + (when-let (info (and contents + (eglot--hover-info contents + range))) (eldoc-message info))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) From 2190da46201ec01b58f9524e0e07172fbf44e3de Mon Sep 17 00:00:00 2001 From: Evgeni Kolev Date: Sat, 18 Aug 2018 14:33:13 +0300 Subject: [PATCH 288/771] Add go-langserver () Copyright-paperwork-exempt: yes * README.md (Installation and usage): Add go-langserver. * eglot.el (eglot-server-programs): Add go-langserver. GitHub-reference: https://github.com/joaotavora/eglot/issues/74 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3fd98c7d800..d38e2503282 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -91,7 +91,8 @@ (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) (haskell-mode . ("hie-wrapper")) - (kotlin-mode . ("kotlin-language-server"))) + (kotlin-mode . ("kotlin-language-server")) + (go-mode . ("go-langserver" "-mode=stdio" "-gocodecompletion"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From 667821d2963eb7123bb9f76909247985d3435ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 19 Aug 2018 02:11:09 +0100 Subject: [PATCH 289/771] Improve snippet support * eglot.el (eglot-client-capabilities): Don't always declare snippet support. (eglot--snippet-expansion-fn): New helper. (eglot-completion-at-point): Better annotations when snippets are supported. --- lisp/progmodes/eglot.el | 60 ++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d38e2503282..3bd342dde6f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -204,7 +204,11 @@ let the buffer grow forever." :dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t) :completion (list :dynamicRegistration :json-false - :completionItem `(:snippetSupport t)) + :completionItem + `(:snippetSupport + ,(if (eglot--snippet-expansion-fn) + t + :json-false))) :hover `(:dynamicRegistration :json-false) :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) @@ -698,6 +702,13 @@ If optional MARKER, return a marker instead" (let ((retval (url-filename (url-generic-parse-url (url-unhex-string uri))))) (if (eq system-type 'windows-nt) (substring retval 1) retval))) +(defun eglot--snippet-expansion-fn () + "Compute a function to expand snippets. +Doubles as an indicator of snippet support." + (and (boundp 'yas-minor-mode) + (symbol-value 'yas-minor-mode) + 'yas-expand-snippet)) + (defconst eglot--kind-names `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") @@ -1387,25 +1398,36 @@ is not active." :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) (mapcar - (jsonrpc-lambda (&rest all &key label insertText &allow-other-keys) - (let ((insert (or insertText (string-trim-left label)))) - (add-text-properties 0 1 all insert) - (put-text-property 0 1 'eglot--lsp-completion all insert) - insert)) + (jsonrpc-lambda (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) items)))) :annotation-function (lambda (obj) - (cl-destructuring-bind (&key detail documentation kind &allow-other-keys) + (cl-destructuring-bind (&key detail kind insertTextFormat + &allow-other-keys) (text-properties-at 0 obj) - (let ((annotation - (or (and documentation - (replace-regexp-in-string - "\n.*" "" (eglot--format-markup documentation))) - detail - (cdr (assoc kind eglot--kind-names))))) + (let* ((detail (and (stringp detail) + (not (string= detail "")) + detail)) + (annotation + (or detail + (cdr (assoc kind eglot--kind-names))))) (when annotation - (concat " " (propertize annotation - 'face 'font-lock-function-name-face)))))) + (concat " " + (propertize annotation + 'face 'font-lock-function-name-face) + (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn) + " (snippet)")))))) :display-sort-function (lambda (items) (sort items (lambda (a b) @@ -1434,10 +1456,10 @@ is not active." insertText &allow-other-keys) (text-properties-at 0 obj) - (when (and (eql insertTextFormat 2) - (fboundp 'yas-expand-snippet)) - (delete-region (- (point) (length obj)) (point)) - (funcall 'yas-expand-snippet insertText)) + (when-let ((fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (delete-region (car bounds) (point)) + (funcall fn insertText)) (eglot--signal-textDocument/didChange) (eglot-eldoc-function))))))) From 9fa0dd072aa3277686ec581ae3075475e02dada6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 20 Aug 2018 01:10:11 +0100 Subject: [PATCH 290/771] Consider :triggercharacters in company completion * eglot.el (eglot-completion-at-point): Take advantage of :company-prefix-length. GitHub-reference: close https://github.com/joaotavora/eglot/issues/80 --- lisp/progmodes/eglot.el | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3bd342dde6f..4f726440e29 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1384,8 +1384,9 @@ is not active." (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) - (server (eglot--current-server-or-lose))) - (when (eglot--server-capable :completionProvider) + (server (eglot--current-server-or-lose)) + (completion-capability (eglot--server-capable :completionProvider))) + (when completion-capability (list (or (car bounds) (point)) (or (cdr bounds) (point)) @@ -1451,6 +1452,10 @@ is not active." (erase-buffer) (insert (eglot--format-markup documentation)) (current-buffer))))) + :company-prefix-length + (cl-some #'looking-back + (mapcar #'regexp-quote + (plist-get completion-capability :triggerCharacters))) :exit-function (lambda (obj _status) (cl-destructuring-bind (&key insertTextFormat insertText From 78102bc38ad796b92e03305af7510074b2ba1476 Mon Sep 17 00:00:00 2001 From: Phillip Dixon Date: Mon, 20 Aug 2018 21:21:51 +1200 Subject: [PATCH 291/771] Ignore extra keys in textdocument/publishdiagnostics () Accoding to the "discussion" in https://reviews.llvm.org/D50571, it was deemed sufficient that VSCode is fine with the non-standard extension -- jt Copyright-paperwork-exempt: yes * eglot.el (eglot-handle-notification): Add &allow-other-keys GitHub-reference: https://github.com/joaotavora/eglot/issues/81 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4f726440e29..eba1b1d1cf9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1036,7 +1036,8 @@ COMMAND is a symbol naming the command." (cl-loop for diag-spec across diagnostics collect (cl-destructuring-bind (&key range ((:severity sev)) _group - _code source message) + _code source message + &allow-other-keys) diag-spec (setq message (concat source ": " message)) (pcase-let From 93ca152da7dbef8a58a00277a4f7a23222bf0055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 20 Aug 2018 23:51:27 +0100 Subject: [PATCH 292/771] Correctly delete text before expanding snippet completions Suggested by Amol Mandhane. * eglot.el (eglot-completion-at-point): Use length of obj in :exit-function GitHub-reference: close https://github.com/joaotavora/eglot/issues/82 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eba1b1d1cf9..75871640687 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1464,7 +1464,7 @@ is not active." (text-properties-at 0 obj) (when-let ((fn (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)))) - (delete-region (car bounds) (point)) + (delete-region (- (point) (length obj)) (point)) (funcall fn insertText)) (eglot--signal-textDocument/didChange) (eglot-eldoc-function))))))) From d01b5110dc6d0cf713760dbaf26ce0158478ad11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 25 Aug 2018 22:57:22 +0100 Subject: [PATCH 293/771] Handle case when :textdocumentsync isn't a number Also closes https://github.com/joaotavora/eglot/issues/87. * eglot.el (eglot--signal-textDocument/didChange): Grab :change from :textDocumentSync server capability. GitHub-reference: close https://github.com/joaotavora/eglot/issues/86 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 75871640687..31cce816a1a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1191,7 +1191,9 @@ When called interactively, use the currently active server" "Send textDocument/didChange to server." (when eglot--recent-changes (let* ((server (eglot--current-server-or-lose)) - (sync-kind (eglot--server-capable :textDocumentSync)) + (sync-capability (eglot--server-capable :textDocumentSync)) + (sync-kind (if (numberp sync-capability) sync-capability + (plist-get sync-capability :change))) (full-sync-p (or (eq sync-kind 1) (eq :emacs-messup eglot--recent-changes)))) (jsonrpc-notify From 6499223125ed680ab5721c5aad55561fe4c37121 Mon Sep 17 00:00:00 2001 From: Evgeni Kolev Date: Mon, 27 Aug 2018 16:17:09 +0300 Subject: [PATCH 294/771] When exiting emacs, don't ask the user to confirm killing processes () Copyright-paperwork-exempt: yes * eglot.el (eglot--connect, eglot--inferior-bootstrap): pass noquery t to make-process. GitHub-reference: https://github.com/joaotavora/eglot/issues/83 --- lisp/progmodes/eglot.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 31cce816a1a..4ff7337d11e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -513,6 +513,7 @@ This docstring appeases checkdoc, that's all." :command contact :connection-type 'pipe :coding 'utf-8-emacs-unix + :noquery t :stderr (get-buffer-create (format "*%s stderr*" readable-name)))))))) (spread @@ -627,6 +628,7 @@ CONNECT-ARGS are passed as additional arguments to (make-process :name (format "autostart-inferior-%s" name) :stderr (format "*%s stderr*" name) + :noquery t :command (cl-subst (format "%s" port-number) :autoport contact))) (setq connection From acda0eda5a8599d8a843eb3c9dc214d80593853f Mon Sep 17 00:00:00 2001 From: Fangrui Song Date: Fri, 7 Sep 2018 02:00:18 -0700 Subject: [PATCH 295/771] Don't warn on implementation-specific notifications () MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only warn when method name doesn't start with '$'. Per the spec: "if a server or client receives notifications or requests starting with ‘$/’ it is free to ignore them if they are unknown." * eglot.el (eglot-handle-notification t t): Check method name for $. GitHub-reference: https://github.com/joaotavora/eglot/issues/93 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4ff7337d11e..2ef22593e06 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -987,7 +987,8 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defmethod eglot-handle-notification (_server method &key &allow-other-keys) "Handle unknown notification" - (eglot--warn "Server sent unknown notification method `%s'" method)) + (unless (string-prefix-p "$" method) + (eglot--warn "Server sent unknown notification method `%s'" method))) (cl-defmethod eglot-handle-request (_server method &key &allow-other-keys) From ef5e1235f657f224468ea9ba32cfff2d46475fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 7 Sep 2018 12:22:14 +0100 Subject: [PATCH 296/771] Fix serious breakage introduced by string-prefix-p doesn't work on symbols * eglot.el (eglot-handle-notification): Coerce method name to string. GitHub-reference: https://github.com/joaotavora/eglot/issues/93 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ef22593e06..14c32db9eef 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -987,7 +987,7 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defmethod eglot-handle-notification (_server method &key &allow-other-keys) "Handle unknown notification" - (unless (string-prefix-p "$" method) + (unless (string-prefix-p "$" (format "%s" method)) (eglot--warn "Server sent unknown notification method `%s'" method))) (cl-defmethod eglot-handle-request From 2d6b24bfa67190e0364ecaba0298dac891992f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 7 Sep 2018 12:46:46 +0100 Subject: [PATCH 297/771] Prefer ccls over cquery for c/c++ * README.md (Installation and usage): Prefer ccls to cquery. Mention clangd. * eglot.el (eglot-server-programs): Suggest ccls for c/c++ by default. GitHub-reference: close https://github.com/joaotavora/eglot/issues/94 --- lisp/progmodes/eglot.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 14c32db9eef..49cfce28ce3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -83,8 +83,7 @@ js2-mode rjsx-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) - ((c++-mode - c-mode) . (eglot-cquery "cquery")) + ((c++-mode c-mode) . ("ccls")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) From 5c9fb5c3982f266c55a3ab472b296496389c95f0 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sat, 8 Sep 2018 23:19:29 +0100 Subject: [PATCH 298/771] Don't send other notifications before initialized Copyright-paperwork-exempt: yes * eglot.el (eglot--connect): send initialized before activating minor mode. GitHub-reference: close https://github.com/joaotavora/eglot/issues/100 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 49cfce28ce3..1a702543b96 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -559,10 +559,10 @@ This docstring appeases checkdoc, that's all." (push server (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) + (jsonrpc-notify server :initialized `(:__dummy__ t)) (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) - (jsonrpc-notify server :initialized `(:__dummy__ t)) (setf (eglot--inhibit-autoreconnect server) (cond ((booleanp eglot-autoreconnect) From 4771f2f6850cc5c2402e0f88ae85a29ab1d76709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 17 Sep 2018 10:00:16 +0100 Subject: [PATCH 299/771] Don't block kill-buffer-hook if server somehow hangs * eglot.el (eglot--signal-textDocument/didClose): Use with-demoted-errors. GitHub-reference: close https://github.com/joaotavora/eglot/issues/115 --- lisp/progmodes/eglot.el | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1a702543b96..959cc5d1b8d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1223,9 +1223,11 @@ When called interactively, use the currently active server" (defun eglot--signal-textDocument/didClose () "Send textDocument/didClose to server." - (jsonrpc-notify - (eglot--current-server-or-lose) - :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier)))) + (with-demoted-errors + "[eglot] error sending textDocument/didClose: %s" + (jsonrpc-notify + (eglot--current-server-or-lose) + :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier))))) (defun eglot--signal-textDocument/willSave () "Send textDocument/willSave to server." From 169360c2570c31bd3dcf8d32b754cd15ca51d3e4 Mon Sep 17 00:00:00 2001 From: whatacold Date: Mon, 24 Sep 2018 20:21:47 +0800 Subject: [PATCH 300/771] Autoload eglot-ensure () Copyright-paperwork-exempt: yes * eglot.el (eglot-ensure): Add autoload cookie GitHub-reference: https://github.com/joaotavora/eglot/issues/120 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 959cc5d1b8d..8e4be897b78 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -446,6 +446,7 @@ INTERACTIVE is t if called interactively." (defvar eglot--managed-mode) ; forward decl +;;;###autoload (defun eglot-ensure () "Start Eglot session for current buffer if there isn't one." (let ((buffer (current-buffer))) From 72eae8b7de1d7c7257748f48ec960481f6223eef Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Mon, 24 Sep 2018 14:24:19 +0200 Subject: [PATCH 301/771] Correctly map documentsymbol's :kind to its name () Previously we were mapping :kind in DocumentSymbol with names from the CompletionItemKind enum, whereas we should have used the SymbolKind enum. * eglot.el (eglot--symbol-kind-names): New variable. (eglot-imenu): Use it instead of eglot--kind-names. GitHub-reference: https://github.com/joaotavora/eglot/issues/121 --- lisp/progmodes/eglot.el | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8e4be897b78..d2ec9291b0a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -718,6 +718,17 @@ Doubles as an indicator of snippet support." (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") (17 . "File") (18 . "Reference"))) +(defconst eglot--symbol-kind-names + `((1 . "File") (2 . "Module") + (3 . "Namespace") (4 . "Package") (5 . "Class") + (6 . "Method") (7 . "Property") (8 . "Field") + (9 . "Constructor") (10 . "Enum") (11 . "Interface") + (12 . "Function") (13 . "Variable") (14 . "Constant") + (15 . "String") (16 . "Number") (17 . "Boolean") + (18 . "Array") (19 . "Object") (20 . "Key") + (21 . "Null") (22 . "EnumMember") (23 . "Struct") + (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) + (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) @@ -1580,7 +1591,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapcar (jsonrpc-lambda (&key name kind location _containerName) - (cons (propertize name :kind (cdr (assoc kind eglot--kind-names))) + (cons (propertize name :kind (cdr (assoc kind eglot--symbol-kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) (jsonrpc-request (eglot--current-server-or-lose) From d294a3e010a955edb9e26537b144f8aabe4b5f99 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Wed, 3 Oct 2018 21:08:38 +0200 Subject: [PATCH 302/771] Make eglot-ignored-server-capabilites more user-friendly () * eglot.el (eglot-ignored-server-capabilites): Add list of possible choices to :type, along with a user-friendly description. GitHub-reference: https://github.com/joaotavora/eglot/issues/126 --- lisp/progmodes/eglot.el | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d2ec9291b0a..8014024ecd8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -745,7 +745,29 @@ Doubles as an indicator of snippet support." You could add, for instance, the symbol `:documentHighlightProvider' to prevent automatic highlighting under cursor." - :type '(repeat symbol)) + :type '(repeat + (choice + (symbol :tag "Other") + (const :tag "Documentation on hover" :hoverProvider) + (const :tag "Code completion" :completionProvider) + (const :tag "Function signature help" :signatureHelpProvider) + (const :tag "Go to definition" :definitionProvider) + (const :tag "Go to type definition" :typeDefinitionProvider) + (const :tag "Go to implementation" :implementationProvider) + (const :tag "Find references" :referencesProvider) + (const :tag "Highlight symbols automatically" :documentHighlightProvider) + (const :tag "List symbols in buffer" :documentSymbolProvider) + (const :tag "List symbols in workspace" :workspaceSymbolProvider) + (const :tag "Execute code actions" :codeActionProvider) + (const :tag "Code lens" :codeLensProvider) + (const :tag "Format buffer" :documentFormattingProvider) + (const :tag "Format portion of buffer" :documentRangeFormattingProvider) + (const :tag "On-type formatting" :documentOnTypeFormattingProvider) + (const :tag "Rename symbol" :renameProvider) + (const :tag "Highlight links in document" :documentLinkProvider) + (const :tag "Decorate color references" :colorProvider) + (const :tag "Fold regions of buffer" :foldingRangeProvider) + (const :tag "Execute custom commands" :executeCommandProvider)))) (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." From aa8653cc3f0ecc0c0652fedc7e9d7835aabee8ed Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Mon, 15 Oct 2018 22:58:14 +0200 Subject: [PATCH 303/771] Eglot-ignored-server-capabilites: prefer all choices over "other" Previously the "Other" choice matched every value, so it was always shown in the customize buffer. * eglot.el (eglot-ignored-server-capabilites): Make the "Other" choice the last possible option. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8014024ecd8..b6606830165 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -747,7 +747,6 @@ You could add, for instance, the symbol under cursor." :type '(repeat (choice - (symbol :tag "Other") (const :tag "Documentation on hover" :hoverProvider) (const :tag "Code completion" :completionProvider) (const :tag "Function signature help" :signatureHelpProvider) @@ -767,7 +766,8 @@ under cursor." (const :tag "Highlight links in document" :documentLinkProvider) (const :tag "Decorate color references" :colorProvider) (const :tag "Fold regions of buffer" :foldingRangeProvider) - (const :tag "Execute custom commands" :executeCommandProvider)))) + (const :tag "Execute custom commands" :executeCommandProvider) + (symbol :tag "Other")))) (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." From 3d9a6f1e79e76bd69c3f3ada4d964233d16730fb Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sun, 9 Sep 2018 00:24:49 +0200 Subject: [PATCH 304/771] Handle case when diagnostic :character is out of range * eglot.el (eglot-handle-notification): Don't error out when flymake-diag-region returns nil. --- lisp/progmodes/eglot.el | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8014024ecd8..b25d8b90df4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1080,14 +1080,21 @@ COMMAND is a symbol naming the command." ((`(,beg . ,end) (eglot--range-region range))) ;; Fallback to `flymake-diag-region' if server ;; botched the range - (if (= beg end) - (let* ((st (plist-get range :start)) - (diag-region - (flymake-diag-region - (current-buffer) (1+ (plist-get st :line)) - (plist-get st :character)))) - (setq beg (car diag-region) - end (cdr diag-region)))) + (when (= beg end) + (if-let* ((st (plist-get range :start)) + (diag-region + (flymake-diag-region + (current-buffer) (1+ (plist-get st :line)) + (plist-get st :character)))) + (setq beg (car diag-region) end (cdr diag-region)) + (eglot--widening + (goto-char (point-min)) + (setq beg + (point-at-bol + (1+ (plist-get (plist-get range :start) :line)))) + (setq end + (point-at-eol + (1+ (plist-get (plist-get range :end) :line))))))) (eglot--make-diag (current-buffer) beg end (cond ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) From 3a24bc0f4f65b3729b239776b2bdae00edd5884a Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sat, 8 Sep 2018 21:36:00 +0200 Subject: [PATCH 305/771] Sort references and definitions by line number * eglot.el (eglot--sort-xrefs): New function. (xref-backend-definitions): (xref-backend-references): (xref-backend-apropos): Use it. --- lisp/progmodes/eglot.el | 47 ++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 41863cc213f..7e79b165c69 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1323,6 +1323,12 @@ DUMMY is ignored." ;; F!@(#*&#$)CKING OFF-BY-ONE again (1+ line) character)))) +(defun eglot--sort-xrefs (xrefs) + (sort xrefs + (lambda (a b) + (< (xref-location-line (xref-item-location a)) + (xref-location-line (xref-item-location b)))))) + (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) (let ((server (eglot--current-server-or-lose)) @@ -1363,9 +1369,10 @@ DUMMY is ignored." :textDocument/definition (get-text-property 0 :textDocumentPositionParams identifier))))) - (mapcar (jsonrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - location-or-locations))) + (eglot--sort-xrefs + (mapcar (jsonrpc-lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + location-or-locations)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) @@ -1376,25 +1383,27 @@ DUMMY is ignored." (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (mapcar - (jsonrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/references - (append - params - (list :context - (list :includeDeclaration t))))))) + (eglot--sort-xrefs + (mapcar + (jsonrpc-lambda (&key uri range) + (eglot--xref-make identifier uri (plist-get range :start))) + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/references + (append + params + (list :context + (list :includeDeclaration t)))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (mapcar - (jsonrpc-lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) - (jsonrpc-request (eglot--current-server-or-lose) - :workspace/symbol - `(:query ,pattern))))) + (eglot--sort-xrefs + (mapcar + (jsonrpc-lambda (&key name location &allow-other-keys) + (cl-destructuring-bind (&key uri range) location + (eglot--xref-make name uri (plist-get range :start)))) + (jsonrpc-request (eglot--current-server-or-lose) + :workspace/symbol + `(:query ,pattern)))))) (defun eglot-format-buffer () "Format contents of current buffer." From ebacb5f8614ff2db2371da98ede733502847e3e8 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sat, 6 Oct 2018 17:08:04 +0200 Subject: [PATCH 306/771] Improve signature help * eglot.el (eglot--sig-info): Don't lose existing information. Attempt to highlight the active parameter by searching for it's :label in signature's :label. Append to the result first sentence of signature's :documentation, if present. --- lisp/progmodes/eglot.el | 52 +++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7e79b165c69..57d19b33c1d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1537,23 +1537,41 @@ is not active." (defun eglot--sig-info (sigs active-sig active-param) (cl-loop for (sig . moresigs) on (append sigs nil) for i from 0 - concat (cl-destructuring-bind (&key label _documentation parameters) sig - (let (active-doc) - (concat - (propertize (replace-regexp-in-string "(.*$" "(" label) - 'face 'font-lock-function-name-face) - (cl-loop - for (param . moreparams) on (append parameters nil) for j from 0 - concat (cl-destructuring-bind (&key label documentation) param - (when (and (eql j active-param) (eql i active-sig)) - (setq label (propertize - label - 'face 'eldoc-highlight-function-argument)) - (when documentation - (setq active-doc (concat label ": " documentation)))) - label) - if moreparams concat ", " else concat ")") - (when active-doc (concat "\n" active-doc))))) + concat (cl-destructuring-bind (&key label documentation parameters) sig + (with-temp-buffer + (save-excursion (insert label)) + (when (looking-at "\\([^(]+\\)(") + (add-face-text-property (match-beginning 1) (match-end 1) + 'font-lock-function-name-face)) + + (when (and (stringp documentation) (eql i active-sig) + (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" + documentation)) + (setq documentation (match-string 1 documentation)) + (unless (string-prefix-p (string-trim documentation) label) + (goto-char (point-max)) + (insert ": " documentation))) + (when (and (eql i active-sig) active-param + (< -1 active-param (length parameters))) + (cl-destructuring-bind (&key label documentation) + (aref parameters active-param) + (goto-char (point-min)) + (let ((case-fold-search nil)) + (cl-loop for nmatches from 0 + while (and (not (string-empty-p label)) + (search-forward label nil t)) + finally do + (when (= 1 nmatches) + (add-face-text-property + (- (point) (length label)) (point) + 'eldoc-highlight-function-argument)))) + (when documentation + (goto-char (point-max)) + (insert "\n" + (propertize + label 'face 'eldoc-highlight-function-argument) + ": " documentation)))) + (buffer-string))) when moresigs concat "\n")) (defun eglot-help-at-point () From 563011ec640f91799c22b9d9bb5b6eb6087dd5bd Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Thu, 9 Aug 2018 12:20:36 +0200 Subject: [PATCH 307/771] Allow function contacts to be interactive * eglot.el (eglot-server-programs): Mention that the function must accept one argument. (eglot--guess-contact): Pass to functional contacts the interactive value. GitHub-reference: per https://github.com/joaotavora/eglot/issues/63 --- lisp/progmodes/eglot.el | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 57d19b33c1d..4376d3af7b0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -124,8 +124,14 @@ of those modes. CONTACT can be: `jsonrpc-process-connection', which you should see for the semantics of the mandatory :PROCESS argument. -* A function of no arguments producing any of the above values - for CONTACT.") +* A function of a single argument producing any of the above + values for CONTACT. The argument's value is non-nil if the + connection was requested interactively (e.g. from the `eglot' + command), and nil if it wasn't (e.g. from `eglot-ensure'). If + the call is interactive, the function can ask the user for + hints on finding the required programs, etc. Otherwise, it + should not ask the user for any input, and return nil or signal + an error if it can't produce a valid CONTACT.") (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) @@ -353,7 +359,9 @@ be guessed." (lambda (m1 m2) (or (eq m1 m2) (and (listp m1) (memq m2 m1))))))) - (guess (if (functionp guess) (funcall guess) guess)) + (guess (if (functionp guess) + (funcall guess interactive) + guess)) (class (or (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) From 5423eed9e7160b363e55e24d910b51e98c1f2bd0 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Thu, 9 Aug 2018 12:21:11 +0200 Subject: [PATCH 308/771] Add support for eclipse.jdt.ls server * eglot.el (eglot-server-programs): Add java-mode entry. (eglot-eclipse-jdt): New class. (eglot-initialization-options): Override for eglot-eclipse-jdt. (eglot--eclipse-jdt-contact): New function. GitHub-reference: per https://github.com/joaotavora/eglot/issues/63 --- lisp/progmodes/eglot.el | 94 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4376d3af7b0..daae4542295 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -91,7 +91,9 @@ language-server/bin/php-language-server.php")) (haskell-mode . ("hie-wrapper")) (kotlin-mode . ("kotlin-language-server")) - (go-mode . ("go-langserver" "-mode=stdio" "-gocodecompletion"))) + (go-mode . ("go-langserver" "-mode=stdio" + "-gocodecompletion")) + (java-mode . eglot--eclipse-jdt-contact)) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated @@ -1892,6 +1894,96 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (list :cacheDirectory (file-name-as-directory cache) :progressReportFrequencyMs -1))) + +;;; eclipse-jdt-specific +;;; +(defclass eglot-eclipse-jdt (eglot-lsp-server) () + :documentation "Eclipse's Java Development Tools Language Server.") + +(cl-defmethod eglot-initialization-options ((server eglot-eclipse-jdt)) + "Passes through required jdt initialization options" + `(:workspaceFolders + [,@(cl-delete-duplicates + (mapcar #'eglot--path-to-uri + (let* ((roots (project-roots (eglot--project server))) + (root (car roots))) + (append + roots + (mapcar + #'file-name-directory + (append + (file-expand-wildcards (concat root "*/pom.xml")) + (file-expand-wildcards (concat root "*/build.gradle")) + (file-expand-wildcards (concat root "*/.project"))))))) + :test #'string=)] + ,@(if-let ((home (or (getenv "JAVA_HOME") + (ignore-errors + (expand-file-name + ".." + (file-name-directory + (file-chase-links (executable-find "javac")))))))) + `(:settings (:java (:home ,home))) + (ignore (eglot--warn "JAVA_HOME env var not set"))))) + +(defun eglot--eclipse-jdt-contact (interactive) + "Return a contact for connecting to eclipse.jdt.ls server, as a cons cell." + (cl-labels + ((is-the-jar + (path) + (and (string-match-p + "org\\.eclipse\\.equinox\\.launcher_.*\\.jar$" + (file-name-nondirectory path)) + (file-exists-p path)))) + (let* ((classpath (or (getenv "CLASSPATH") ":")) + (cp-jar (cl-find-if #'is-the-jar (split-string classpath ":"))) + (jar cp-jar) + (dir + (cond + (jar (file-name-as-directory + (expand-file-name ".." (file-name-directory jar)))) + (interactive + (expand-file-name + (read-directory-name + (concat "Path to eclipse.jdt.ls directory (could not" + " find it in CLASSPATH): ") + nil nil t))) + (t (error "Could not find eclipse.jdt.ls jar in CLASSPATH")))) + (repodir + (concat dir + "org.eclipse.jdt.ls.product/target/repository/")) + (repodir (if (file-directory-p repodir) repodir dir)) + (config + (concat + repodir + (cond + ((string= system-type "darwin") "config_mac") + ((string= system-type "windows-nt") "config_win") + (t "config_linux")))) + (workspace + (expand-file-name (md5 (car (project-roots (project-current)))) + (concat user-emacs-directory + "eglot-eclipse-jdt-cache")))) + (unless jar + (setq jar + (cl-find-if #'is-the-jar + (directory-files (concat repodir "plugins") t)))) + (unless (and jar (file-exists-p jar) (file-directory-p config)) + (error "Could not find required eclipse.jdt.ls files (build required?)")) + (when (and interactive (not cp-jar) + (y-or-n-p (concat "Add path to the server program " + "to CLASSPATH environment variable?"))) + (setenv "CLASSPATH" (concat (getenv "CLASSPATH") ":" jar))) + (unless (file-directory-p workspace) + (make-directory workspace t)) + (cons 'eglot-eclipse-jdt + (list (executable-find "java") + "-Declipse.application=org.eclipse.jdt.ls.core.id1" + "-Dosgi.bundles.defaultStartLevel=4" + "-Declipse.product=org.eclipse.jdt.ls.core.product" + "-jar" jar + "-configuration" config + "-data" workspace))))) + ;; FIXME: A horrible hack of Flymake's insufficient API that must go ;; into Emacs master, or better, 26.2 From c726fc7a9ccc857aefc97282b30eef94266c2f93 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Thu, 9 Aug 2018 15:25:58 +0200 Subject: [PATCH 309/771] Override eglot-execute-command for eclipse.jdt.ls server * eglot.el (eglot-execute-command eglot-eclipse-jdt): New defmethod. --- lisp/progmodes/eglot.el | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index daae4542295..a23e73df835 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1984,6 +1984,11 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "-configuration" config "-data" workspace))))) +(cl-defmethod eglot-execute-command + ((_server eglot-eclipse-jdt) (_cmd (eql java.apply.workspaceEdit)) arguments) + "Eclipse JDT breaks spec and replies with edits as arguments." + (mapc #'eglot--apply-workspace-edit arguments)) + ;; FIXME: A horrible hack of Flymake's insufficient API that must go ;; into Emacs master, or better, 26.2 From f3c43b4cac387384359322d1e21e5410a16b3eae Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Tue, 11 Sep 2018 23:13:56 +0200 Subject: [PATCH 310/771] Handle case when project was not found in eclipse.jdt.ls contact --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a23e73df835..75d38573e9a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1959,8 +1959,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ((string= system-type "darwin") "config_mac") ((string= system-type "windows-nt") "config_win") (t "config_linux")))) + (project (or (project-current) `(transient . ,default-directory))) (workspace - (expand-file-name (md5 (car (project-roots (project-current)))) + (expand-file-name (md5 (car (project-roots project))) (concat user-emacs-directory "eglot-eclipse-jdt-cache")))) (unless jar From eae904fc9cc1d51eee4b7f08cc4e83d130e60a7c Mon Sep 17 00:00:00 2001 From: Dale Sedivec Date: Sun, 21 Oct 2018 15:02:47 -0500 Subject: [PATCH 311/771] Fix misspelling of "outstanding" () Copyright-paperwork-exempt: yes * eglot.el (eglot--mode-line-format): Fix a typo. GitHub-reference: https://github.com/joaotavora/eglot/issues/74 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 75d38573e9a..3d77a4faf4a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1000,7 +1000,7 @@ Uses THING, FACE, DEFS and PREPEND." 'compilation-mode-line-run '()))) ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props - (format "%d oustanding requests" pending) 'warning + (format "%d outstanding requests" pending) 'warning '((mouse-3 eglot-forget-pending-continuations "fahgettaboudit")))))))))) From 1f865ee560c75cb06807b0598043303d36c62d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 30 Oct 2018 12:22:32 +0000 Subject: [PATCH 312/771] Accept deprecated field in symbolinformation * eglot.el (xref-backend-identifier-completion-table) (eglot-imenu): Accept and ignore "deprecated" GitHub-reference: fix https://github.com/joaotavora/eglot/issues/138 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d77a4faf4a..297d6f4f0ea 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1348,7 +1348,7 @@ DUMMY is ignored." (setq eglot--xref-known-symbols (mapcar (jsonrpc-lambda - (&key name kind location containerName) + (&key name kind location containerName _deprecated) (propertize name :textDocumentPositionParams (list :textDocument text-id @@ -1656,7 +1656,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let ((entries (mapcar (jsonrpc-lambda - (&key name kind location _containerName) + (&key name kind location _containerName _deprecated) (cons (propertize name :kind (cdr (assoc kind eglot--symbol-kind-names))) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) From d84d55e6e4c239faafc4f4de5e0295a8b99e15af Mon Sep 17 00:00:00 2001 From: Alex Branham Date: Tue, 30 Oct 2018 16:59:05 -0500 Subject: [PATCH 313/771] Require subr-x at compile time () if-let and when-let are macros that the byte compiler can expand at compile time. No need to require subr-x at run time. * eglot.el (subr-x): Require only when compiling. GitHub-reference: https://github.com/joaotavora/eglot/issues/139 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 297d6f4f0ea..a234e17c0b1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -65,7 +65,8 @@ (require 'warnings) (require 'flymake) (require 'xref) -(require 'subr-x) +(eval-when-compile + (require 'subr-x)) (require 'jsonrpc) (require 'filenotify) (require 'ert) From 5f250e875a7ff176e82290c828496127dfa355b0 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Wed, 31 Oct 2018 12:54:26 +0100 Subject: [PATCH 314/771] Remove duplicates from imenu * eglot.el (eglot-imenu): Don't append the result list to itself, which causes duplicates. --- lisp/progmodes/eglot.el | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a234e17c0b1..2b39849f9bc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1664,12 +1664,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) - (append - (cl-remove nil - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries) - :key #'car) - entries)) + (cl-remove nil + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries) + :key #'car)) (funcall oldfun))) (defun eglot--apply-text-edits (edits &optional version) From 3d91b57629d3f4ca307a561eb05a587e831d8554 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Wed, 31 Oct 2018 13:06:59 +0100 Subject: [PATCH 315/771] Don't ignore unknown symbolkinds in imenu Some servers provide custom SymbolKinds. For example, ccls says that symbols defined with #define are of kind 255. * eglot.el (eglot-imenu): Don't delete elements with unknown symbol kind from the return list, instead put them in `(Unknown)` group. --- lisp/progmodes/eglot.el | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2b39849f9bc..26ee814755a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1658,16 +1658,16 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapcar (jsonrpc-lambda (&key name kind location _containerName _deprecated) - (cons (propertize name :kind (cdr (assoc kind eglot--symbol-kind-names))) + (cons (propertize + name :kind (alist-get kind eglot--symbol-kind-names + "(Unknown)")) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) - (cl-remove nil - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries) - :key #'car)) + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries)) (funcall oldfun))) (defun eglot--apply-text-edits (edits &optional version) From 7f062198c8677028c6ade0b68e07e5f26a3b4867 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Wed, 31 Oct 2018 14:16:56 +0100 Subject: [PATCH 316/771] Use the container name of a symbol in imenu * eglot.el (eglot-imenu): Prepend :containerName to each symbol, when provided. --- lisp/progmodes/eglot.el | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 26ee814755a..e15170400d8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1657,10 +1657,15 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let ((entries (mapcar (jsonrpc-lambda - (&key name kind location _containerName _deprecated) + (&key name kind location containerName _deprecated) (cons (propertize - name :kind (alist-get kind eglot--symbol-kind-names - "(Unknown)")) + (concat + (and (stringp containerName) + (not (string-empty-p containerName)) + (concat containerName "::")) + name) + :kind (alist-get kind eglot--symbol-kind-names + "(Unknown)")) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) (jsonrpc-request (eglot--current-server-or-lose) From 025c926301f5f8f1c5a39fbf22dec1053c90c972 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Wed, 31 Oct 2018 22:14:16 +0100 Subject: [PATCH 317/771] Add support for code action literals Code action literals allow the server to simply return a WorkspaceEdit for a code action, so the client does not have to execute a command. * eglot.el (eglot-client-capabilities): Add :codeActionLiteralSupport. (eglot--code-action-kinds): New variable. (eglot-code-actions): Apply provided WorkspaceEdit. --- lisp/progmodes/eglot.el | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e15170400d8..b2e88d8439f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -223,7 +223,12 @@ let the buffer grow forever." :definition `(:dynamicRegistration :json-false) :documentSymbol `(:dynamicRegistration :json-false) :documentHighlight `(:dynamicRegistration :json-false) - :codeAction `(:dynamicRegistration :json-false) + :codeAction (list + :dynamicRegistration :json-false + :codeActionLiteralSupport + `(:codeActionKind + (:valueSet + [,@eglot--code-action-kinds]))) :formatting `(:dynamicRegistration :json-false) :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) @@ -740,6 +745,11 @@ Doubles as an indicator of snippet support." (21 . "Null") (22 . "EnumMember") (23 . "Struct") (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) +(defconst eglot--code-action-kinds + '("quickfix" "refactor" "refactor.extract" + "refactor.inline" "refactor.rewrite" + "source" "source.organizeImports")) + (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) @@ -1788,8 +1798,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) - (menu-items (mapcar (jsonrpc-lambda (&key title command arguments) - `(,title . (:command ,command :arguments ,arguments))) + (menu-items (mapcar (jsonrpc-lambda (&key title command arguments + edit _kind _diagnostics) + `(,title . (:command ,command :arguments ,arguments + :edit ,edit))) actions)) (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) (command-and-args @@ -1802,10 +1814,13 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eq (setq retval (tmm-prompt menu)) never-mind) (keyboard-quit) retval)))))) - (cl-destructuring-bind (&key _title command arguments) command-and-args + (cl-destructuring-bind (&key _title command arguments edit) command-and-args + (when edit + (eglot--apply-workspace-edit edit)) (if command (eglot-execute-command server (intern command) arguments) - (eglot--message "No code actions here"))))) + (unless edit + (eglot--message "No code actions here")))))) From 9e720cbea4949d65d280881d0e5f45b5a727526d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 4 Nov 2018 12:56:37 +0000 Subject: [PATCH 318/771] Simplify eglot-code-action. fix compilation warning * eglot.el (eglot-code-actions): Simplify. (eglot-client-capabilities): Mention supported codeActionKind's directly. (eglot--code-action-kinds): Remove. --- lisp/progmodes/eglot.el | 77 ++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b2e88d8439f..2b683f8874a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -226,9 +226,12 @@ let the buffer grow forever." :codeAction (list :dynamicRegistration :json-false :codeActionLiteralSupport - `(:codeActionKind + '(:codeActionKind (:valueSet - [,@eglot--code-action-kinds]))) + ["quickfix" + "refactor" "refactor.extract" + "refactor.inline" "refactor.rewrite" + "source" "source.organizeImports"]))) :formatting `(:dynamicRegistration :json-false) :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) @@ -745,11 +748,6 @@ Doubles as an indicator of snippet support." (21 . "Null") (22 . "EnumMember") (23 . "Struct") (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) -(defconst eglot--code-action-kinds - '("quickfix" "refactor" "refactor.extract" - "refactor.inline" "refactor.rewrite" - "source" "source.organizeImports")) - (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) @@ -1786,41 +1784,40 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (unless (eglot--server-capable :codeActionProvider) (eglot--error "Server can't execute code actions!")) (let* ((server (eglot--current-server-or-lose)) - (actions (jsonrpc-request - server - :textDocument/codeAction - (list :textDocument (eglot--TextDocumentIdentifier) - :range (list :start (eglot--pos-to-lsp-position beg) - :end (eglot--pos-to-lsp-position end)) - :context - `(:diagnostics - [,@(mapcar (lambda (diag) - (cdr (assoc 'eglot-lsp-diag - (eglot--diag-data diag)))) - (flymake-diagnostics beg end))])))) - (menu-items (mapcar (jsonrpc-lambda (&key title command arguments - edit _kind _diagnostics) - `(,title . (:command ,command :arguments ,arguments - :edit ,edit))) - actions)) - (menu (and menu-items `("Eglot code actions:" ("dummy" ,@menu-items)))) - (command-and-args - (and menu - (if (listp last-nonmenu-event) - (x-popup-menu last-nonmenu-event menu) - (let ((never-mind (gensym)) retval) - (setcdr (cadr menu) - (cons `("never mind..." . ,never-mind) (cdadr menu))) - (if (eq (setq retval (tmm-prompt menu)) never-mind) - (keyboard-quit) - retval)))))) - (cl-destructuring-bind (&key _title command arguments edit) command-and-args + (actions + (jsonrpc-request + server + :textDocument/codeAction + (list :textDocument (eglot--TextDocumentIdentifier) + :range (list :start (eglot--pos-to-lsp-position beg) + :end (eglot--pos-to-lsp-position end)) + :context + `(:diagnostics + [,@(mapcar (lambda (diag) + (cdr (assoc 'eglot-lsp-diag + (eglot--diag-data diag)))) + (flymake-diagnostics beg end))])))) + (menu-items + (or (mapcar (jsonrpc-lambda (&key title command arguments + edit _kind _diagnostics) + `(,title . (:command ,command :arguments ,arguments + :edit ,edit))) + actions) + (eglot--error "No code actions here"))) + (menu `("Eglot code actions:" ("dummy" ,@menu-items))) + (action (if (listp last-nonmenu-event) + (x-popup-menu last-nonmenu-event menu) + (let ((never-mind (gensym)) retval) + (setcdr (cadr menu) + (cons `("never mind..." . ,never-mind) (cdadr menu))) + (if (eq (setq retval (tmm-prompt menu)) never-mind) + (keyboard-quit) + retval))))) + (cl-destructuring-bind (&key _title command arguments edit) action (when edit (eglot--apply-workspace-edit edit)) - (if command - (eglot-execute-command server (intern command) arguments) - (unless edit - (eglot--message "No code actions here")))))) + (when command + (eglot-execute-command server (intern command) arguments))))) From ee58d92a7de1c8b1914c77177a7d5e755f1de827 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sun, 4 Nov 2018 16:59:05 +0100 Subject: [PATCH 319/771] Fix a bug when response to definitions request is a single location It's valid to return just a single Location for a definitions request. * eglot.el (xref-backend-definitions): Coerce response to a vector. --- lisp/progmodes/eglot.el | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2b683f8874a..f11a14e11c5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1381,17 +1381,20 @@ DUMMY is ignored." (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) (let* ((rich-identifier (car (member identifier eglot--xref-known-symbols))) - (location-or-locations + (definitions (if rich-identifier (get-text-property 0 :locations rich-identifier) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/definition (get-text-property - 0 :textDocumentPositionParams identifier))))) + 0 :textDocumentPositionParams identifier)))) + (locations + (and definitions + (if (vectorp definitions) definitions (vector definitions))))) (eglot--sort-xrefs (mapcar (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri (plist-get range :start))) - location-or-locations)))) + locations)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) (unless (eglot--server-capable :referencesProvider) From 40e4c88dd3461136e7a447e96a0d8af9732f10f8 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sun, 4 Nov 2018 16:34:58 +0100 Subject: [PATCH 320/771] Make imenu hierarchical * eglot.el (eglot-imenu): Use :containerName to build a nested imenu index alist. --- lisp/progmodes/eglot.el | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f11a14e11c5..22f509b81b2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1670,20 +1670,29 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (jsonrpc-lambda (&key name kind location containerName _deprecated) (cons (propertize - (concat - (and (stringp containerName) - (not (string-empty-p containerName)) - (concat containerName "::")) - name) + name :kind (alist-get kind eglot--symbol-kind-names - "(Unknown)")) + "Unknown") + :containerName (and (stringp containerName) + (not (string-empty-p containerName)) + containerName)) (eglot--lsp-position-to-point (plist-get (plist-get location :range) :start)))) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument ,(eglot--TextDocumentIdentifier)))))) - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries)) + (mapcar + (pcase-lambda (`(,kind . ,syms)) + (let ((syms-by-scope (seq-group-by + (lambda (e) + (get-text-property 0 :containerName (car e))) + syms))) + (cons kind (cl-loop for (scope . elems) in syms-by-scope + append (if scope + (list (cons scope elems)) + elems))))) + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries))) (funcall oldfun))) (defun eglot--apply-text-edits (edits &optional version) From 6ea0216c53533b76120574b7892a021b037389cf Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Mon, 5 Nov 2018 19:42:32 +0100 Subject: [PATCH 321/771] * eglot.el (eglot-client-capabilities): mention supported symbolkinds. --- lisp/progmodes/eglot.el | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22f509b81b2..0a789702c7d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -196,6 +196,8 @@ let the buffer grow forever." "JSON object to send under `initializationOptions'" (:method (_s) nil)) ; blank default +(defvar eglot--symbol-kind-names) + (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." (:method (_s) @@ -221,7 +223,11 @@ let the buffer grow forever." :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) - :documentSymbol `(:dynamicRegistration :json-false) + :documentSymbol (list + :dynamicRegistration :json-false + :symbolKind `(:valueSet + [,@(mapcar + #'car eglot--symbol-kind-names)])) :documentHighlight `(:dynamicRegistration :json-false) :codeAction (list :dynamicRegistration :json-false From 753dddc631b62ec321fab8e1690af05547b02a05 Mon Sep 17 00:00:00 2001 From: Mario Rodas Date: Wed, 7 Nov 2018 07:09:58 -0500 Subject: [PATCH 322/771] Support ocaml-language-server out of the box () Copyright-paperwork-exempt: yes * eglot.el (eglot-server-programs): Add ocaml-language-server. * README.md (Installation and usage): Mention ocaml-language-server GitHub-reference: https://github.com/joaotavora/eglot/issues/149 --- lisp/progmodes/eglot.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a789702c7d..2ce9d086e7f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -85,6 +85,8 @@ rjsx-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) ((c++-mode c-mode) . ("ccls")) + ((caml-mode tuareg-mode reason-mode) + . ("ocaml-language-server" "--stdio")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) From ee243c0c80eb2c17be1e2606abf156a0d09a74a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 7 Nov 2018 13:10:29 +0000 Subject: [PATCH 323/771] Move constants to top instead of forward-declaring * eglot.el (eglot--symbol-kind-names, eglot--kind-names): Move to top of file. --- lisp/progmodes/eglot.el | 42 +++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ce9d086e7f..e17717452b4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -174,6 +174,28 @@ let the buffer grow forever." :type '(choice (const :tag "No limit" nil) (integer :tag "Number of characters"))) + +;;; Constants +;;; +(defconst eglot--symbol-kind-names + `((1 . "File") (2 . "Module") + (3 . "Namespace") (4 . "Package") (5 . "Class") + (6 . "Method") (7 . "Property") (8 . "Field") + (9 . "Constructor") (10 . "Enum") (11 . "Interface") + (12 . "Function") (13 . "Variable") (14 . "Constant") + (15 . "String") (16 . "Number") (17 . "Boolean") + (18 . "Array") (19 . "Object") (20 . "Key") + (21 . "Null") (22 . "EnumMember") (23 . "Struct") + (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) + +(defconst eglot--kind-names + `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") + (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") + (9 . "Module") (10 . "Property") (11 . "Unit") (12 . "Value") + (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") + (17 . "File") (18 . "Reference"))) + + ;;; API (WORK-IN-PROGRESS!) ;;; (cl-defmacro eglot--with-live-buffer (buf &rest body) @@ -198,8 +220,6 @@ let the buffer grow forever." "JSON object to send under `initializationOptions'" (:method (_s) nil)) ; blank default -(defvar eglot--symbol-kind-names) - (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." (:method (_s) @@ -738,24 +758,6 @@ Doubles as an indicator of snippet support." (symbol-value 'yas-minor-mode) 'yas-expand-snippet)) -(defconst eglot--kind-names - `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") - (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") - (9 . "Module") (10 . "Property") (11 . "Unit") (12 . "Value") - (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") - (17 . "File") (18 . "Reference"))) - -(defconst eglot--symbol-kind-names - `((1 . "File") (2 . "Module") - (3 . "Namespace") (4 . "Package") (5 . "Class") - (6 . "Method") (7 . "Property") (8 . "Field") - (9 . "Constructor") (10 . "Enum") (11 . "Interface") - (12 . "Function") (13 . "Variable") (14 . "Constant") - (15 . "String") (16 . "Number") (17 . "Boolean") - (18 . "Array") (19 . "Object") (20 . "Key") - (21 . "Null") (22 . "EnumMember") (23 . "Struct") - (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) - (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) From a0365b6f81864488b249de23c2b399e475f312a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 9 Nov 2018 01:46:55 +0000 Subject: [PATCH 324/771] Fix a bug introduced by previous bugfix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes a bug but introduced another when completing a symbol in xref-find-definitions. commit ee58d92a7de1c8b1914c77177a7d5e755f1de827 Author: Michał Krzywkowski Date: Sun Nov 4 16:59:05 2018 +0100 * eglot.el (xref-backend-identifier-completion-table): Use vector. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e17717452b4..576d7f39d31 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1374,7 +1374,7 @@ DUMMY is ignored." :position (plist-get (plist-get location :range) :start)) - :locations (list location) + :locations (vector location) :kind kind :containerName containerName)) (jsonrpc-request server From 9f44e74ca9d2b254fbf9fd1d5ccfde64e2b652f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 12 Nov 2018 22:29:38 +0000 Subject: [PATCH 325/771] Add ability to move to lsp-precise columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also close https://github.com/joaotavora/eglot/issues/125. Idea and much of design contributed by Michał Krzywkowski This introduces the variable eglot-move-to-column-function. According to the standard, LSP column/character offsets are based on a count of UTF-16 code units, not actual visual columns. So when LSP says position 3 of a line containing just \"aXbc\", where X is a multi-byte character, it actually means `b', not `c'. This is what the function `eglot-move-to-lsp-abiding-column' does. However, many servers don't follow the spec this closely, and thus this variable should be set to `move-to-column' in buffers managed by those servers. * eglot.el (eglot-move-to-column-function): New variable. (eglot-move-to-lsp-abiding-column): New function. (eglot--lsp-position-to-point): Use eglot-move-to-column-function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/124 --- lisp/progmodes/eglot.el | 43 +++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 576d7f39d31..f7b9c869743 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -728,16 +728,43 @@ CONNECT-ARGS are passed as additional arguments to :character (- (goto-char (or pos (point))) (line-beginning-position))))) +(defvar eglot-move-to-column-function #'move-to-column + "How to move to a column reported by the LSP server. + +According to the standard, LSP column/character offsets are based +on a count of UTF-16 code units, not actual visual columns. So +when LSP says position 3 of a line containing just \"aXbc\", +where X is a multi-byte character, it actually means `b', not +`c'. This is what the function +`eglot-move-to-lsp-abiding-column' does. + +However, many servers don't follow the spec this closely, and +thus this variable should be set to `move-to-column' in buffers +managed by those servers.") + +(defun eglot-move-to-lsp-abiding-column (column) + "Move to COLUMN abiding by the LSP spec." + (cl-loop + initially (move-to-column column) + with lbp = (line-beginning-position) + for diff = (- column + (/ (- (length (encode-coding-region lbp (point) 'utf-16 t)) + 2) + 2)) + until (zerop diff) + for offset = (max 1 (abs (/ diff 2))) + do (if (> diff 0) (forward-char offset) (backward-char offset)))) + (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. If optional MARKER, return a marker instead" - (save-excursion (goto-char (point-min)) - (forward-line (min most-positive-fixnum - (plist-get pos-plist :line))) - (forward-char (min (plist-get pos-plist :character) - (- (line-end-position) - (line-beginning-position)))) - (if marker (copy-marker (point-marker)) (point)))) + (save-excursion + (goto-char (point-min)) + (forward-line (min most-positive-fixnum + (plist-get pos-plist :line))) + (unless (eobp) ;; if line was excessive leave point at eob + (funcall eglot-move-to-column-function (plist-get pos-plist :character))) + (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) "URIfy PATH." @@ -1040,7 +1067,7 @@ Uses THING, FACE, DEFS and PREPEND." (priority . ,(+ 50 i)) (keymap . ,(let ((map (make-sparse-keymap))) (define-key map [mouse-1] - (eglot--mouse-call 'eglot-code-actions)) + (eglot--mouse-call 'eglot-code-actions)) map))))) From 6393580d7e386787da98fa4fdb0c034bdd06d331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 12 Nov 2018 23:17:22 +0000 Subject: [PATCH 326/771] Complex completions work when chosen from *completions* * eglot.el (eglot-completion-at-point): Make exit-function work even if its arguments was stripped of its properties. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/148 --- lisp/progmodes/eglot.el | 60 ++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f7b9c869743..512e9a689fa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1501,7 +1501,8 @@ is not active." "EGLOT's `completion-at-point' function." (let ((bounds (bounds-of-thing-at-point 'symbol)) (server (eglot--current-server-or-lose)) - (completion-capability (eglot--server-capable :completionProvider))) + (completion-capability (eglot--server-capable :completionProvider)) + strings) (when completion-capability (list (or (car bounds) (point)) @@ -1514,19 +1515,21 @@ is not active." :deferred :textDocument/completion :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (mapcar - (jsonrpc-lambda (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items)))) + (setq + strings + (mapcar + (jsonrpc-lambda (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) + items))))) :annotation-function (lambda (obj) (cl-destructuring-bind (&key detail kind insertTextFormat @@ -1572,17 +1575,24 @@ is not active." (cl-some #'looking-back (mapcar #'regexp-quote (plist-get completion-capability :triggerCharacters))) - :exit-function (lambda (obj _status) - (cl-destructuring-bind (&key insertTextFormat - insertText - &allow-other-keys) - (text-properties-at 0 obj) - (when-let ((fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (delete-region (- (point) (length obj)) (point)) - (funcall fn insertText)) - (eglot--signal-textDocument/didChange) - (eglot-eldoc-function))))))) + :exit-function + (lambda (comp _status) + (let ((comp (if (get-text-property 0 'eglot--lsp-completion comp) + comp + ;; When selecting from the *Completions* + ;; buffer, `comp' won't have any properties. A + ;; lookup should fix that (github#148) + (cl-find comp strings :test #'string=)))) + (cl-destructuring-bind (&key insertTextFormat + insertText + &allow-other-keys) + (text-properties-at 0 comp) + (when-let ((fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (delete-region (- (point) (length comp)) (point)) + (funcall fn insertText)) + (eglot--signal-textDocument/didChange) + (eglot-eldoc-function)))))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") From 35e431c829d7e554e6c0ab38dde59fee870506a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 13 Nov 2018 09:46:47 +0000 Subject: [PATCH 327/771] Tweak solution to with a hint from fangrui song * eglot.el (eglot-move-to-lsp-abiding-column): Simplify slightly. GitHub-reference: https://github.com/joaotavora/eglot/issues/125 --- lisp/progmodes/eglot.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 512e9a689fa..24f8c971f91 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -752,8 +752,7 @@ managed by those servers.") 2) 2)) until (zerop diff) - for offset = (max 1 (abs (/ diff 2))) - do (if (> diff 0) (forward-char offset) (backward-char offset)))) + do (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. From 5c97238692a5dcecbefeeaf21b8a4344caca3275 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Wed, 31 Oct 2018 20:59:30 +0100 Subject: [PATCH 328/771] Add support for textedits in completion * eglot.el (eglot-completion-at-point): Apply the CompletionItem's :textEdit and :additionalTextEdits when they're present. --- lisp/progmodes/eglot.el | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 24f8c971f91..d4a8b6f8a33 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1584,12 +1584,23 @@ is not active." (cl-find comp strings :test #'string=)))) (cl-destructuring-bind (&key insertTextFormat insertText + textEdit + additionalTextEdits &allow-other-keys) (text-properties-at 0 comp) - (when-let ((fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (delete-region (- (point) (length comp)) (point)) - (funcall fn insertText)) + (let ((fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (when (or fn textEdit) + ;; Undo the completion + (delete-region (- (point) (length comp)) (point))) + (cond (textEdit + (cl-destructuring-bind (&key range newText) textEdit + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (delete-region beg end) + (goto-char beg) + (funcall (or fn #'insert) newText))) + (eglot--apply-text-edits additionalTextEdits)) + (fn (funcall fn insertText)))) (eglot--signal-textDocument/didChange) (eglot-eldoc-function)))))))) From 8df3bdd653010aebd7083669d5c2a946c73a4d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 13 Nov 2018 22:08:16 +0000 Subject: [PATCH 329/771] Add ability to report lsp-compliant columns * eglot.el (eglot-current-column-function): New variable. (eglot-lsp-abiding-column): New helper. (eglot--pos-to-lsp-position): Use eglot-current-column-function. (eglot-move-to-column-function): Tweak docstring. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/125 --- lisp/progmodes/eglot.el | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d4a8b6f8a33..2c0b22e5002 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -721,26 +721,42 @@ CONNECT-ARGS are passed as additional arguments to (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) +(defvar eglot-current-column-function #'current-column + "Function to calculate the current column. + +This is the inverse operation of +`eglot-move-to-column-function' (which see). It is a function of +no arguments returning a column number. For buffers managed by +fully LSP-compliant servers, this should be set to +`eglot-lsp-abiding-column', and `current-column' (the default) +for all others.") + +(defun eglot-lsp-abiding-column () + "Calculate current COLUMN as defined by the LSP spec." + (/ (- (length (encode-coding-region (line-beginning-position) + (point) 'utf-16 t)) + 2) + 2)) + (defun eglot--pos-to-lsp-position (&optional pos) "Convert point POS to LSP position." (eglot--widening (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE - :character (- (goto-char (or pos (point))) - (line-beginning-position))))) + :character (progn (when pos (goto-char pos)) + (funcall eglot-current-column-function))))) (defvar eglot-move-to-column-function #'move-to-column - "How to move to a column reported by the LSP server. + "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based on a count of UTF-16 code units, not actual visual columns. So when LSP says position 3 of a line containing just \"aXbc\", where X is a multi-byte character, it actually means `b', not -`c'. This is what the function -`eglot-move-to-lsp-abiding-column' does. +`c'. However, many servers don't follow the spec this closely. -However, many servers don't follow the spec this closely, and -thus this variable should be set to `move-to-column' in buffers -managed by those servers.") +For buffers managed by fully LSP-compliant servers, this should +be set to `eglot-move-to-lsp-abiding-column', and +`move-to-column' (the default) for all others.") (defun eglot-move-to-lsp-abiding-column (column) "Move to COLUMN abiding by the LSP spec." From 46d2bef4b397257ea232af483c45f49d7eab900c Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Sat, 17 Nov 2018 12:12:31 +0100 Subject: [PATCH 330/771] Format documentation of signature parameters * eglot.el (eglot--sig-info): Call `eglot--format-markup` on parameter :documentation. GitHub-reference: per https://github.com/joaotavora/eglot/issues/144 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2c0b22e5002..3ba1b87d807 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1665,7 +1665,7 @@ is not active." (insert "\n" (propertize label 'face 'eldoc-highlight-function-argument) - ": " documentation)))) + ": " (eglot--format-markup documentation))))) (buffer-string))) when moresigs concat "\n")) From 2cf7905887f2137869f44c3383a55636e38b4b81 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Mon, 19 Nov 2018 21:22:14 +0100 Subject: [PATCH 331/771] Treat tab characters as 1 column wide in position conversion functions Fixes https://github.com/joaotavora/eglot/issues/158. * eglot.el (eglot--pos-to-lsp-position): Call eglot-current-column-function with tab-width bound to 1. (eglot--lsp-position-to-point): Call eglot-move-to-column-function with tab-width bound to 1. --- lisp/progmodes/eglot.el | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3ba1b87d807..d9c1c3ab97e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -743,7 +743,8 @@ for all others.") (eglot--widening (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE :character (progn (when pos (goto-char pos)) - (funcall eglot-current-column-function))))) + (let ((tab-width 1)) + (funcall eglot-current-column-function)))))) (defvar eglot-move-to-column-function #'move-to-column "Function to move to a column reported by the LSP server. @@ -778,7 +779,8 @@ If optional MARKER, return a marker instead" (forward-line (min most-positive-fixnum (plist-get pos-plist :line))) (unless (eobp) ;; if line was excessive leave point at eob - (funcall eglot-move-to-column-function (plist-get pos-plist :character))) + (let ((tab-width 1)) + (funcall eglot-move-to-column-function (plist-get pos-plist :character)))) (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) From 0097d8d8327a783c77bed2860c4283541c6608b7 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Wed, 21 Nov 2018 15:54:22 +0100 Subject: [PATCH 332/771] Properly delete inserted text after completion * eglot.el (eglot-completion-at-point): In :exit-function, delete only the region of buffer that was inserted by completion. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/160 --- lisp/progmodes/eglot.el | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d9c1c3ab97e..c65d9089784 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1543,6 +1543,7 @@ is not active." (string-trim-left label)) (t (or insertText (string-trim-left label)))))) + (setq all (append all `(:bounds ,bounds))) (add-text-properties 0 1 all completion) (put-text-property 0 1 'eglot--lsp-completion all completion) completion)) @@ -1604,13 +1605,19 @@ is not active." insertText textEdit additionalTextEdits + bounds &allow-other-keys) (text-properties-at 0 comp) (let ((fn (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)))) (when (or fn textEdit) - ;; Undo the completion - (delete-region (- (point) (length comp)) (point))) + ;; Undo the completion. If before completion the buffer was + ;; "foo.b" and now is "foo.bar", `comp' will be "bar". We + ;; want to delete only "ar" (`comp' minus the symbol whose + ;; bounds we've calculated before) (github#160). + (delete-region (+ (- (point) (length comp)) + (if bounds (- (cdr bounds) (car bounds)) 0)) + (point))) (cond (textEdit (cl-destructuring-bind (&key range newText) textEdit (pcase-let ((`(,beg . ,end) (eglot--range-region range))) From a6536ec8b0c20cab5c04edf7c552077a9d3f6b7d Mon Sep 17 00:00:00 2001 From: Alex Branham Date: Tue, 20 Nov 2018 15:27:38 -0600 Subject: [PATCH 333/771] Add support for r's languageserver () Copyright-paperwork-exempt: yes * eglot.el (eglot-server-programs): Add R language server. * README.md (Installation and usage): Mention it. GitHub-reference: https://github.com/joaotavora/eglot/issues/161 --- lisp/progmodes/eglot.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c65d9089784..c382c67e549 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -96,6 +96,8 @@ language-server/bin/php-language-server.php")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("go-langserver" "-mode=stdio" "-gocodecompletion")) + ((R-mode ess-r-mode) . ("R" "--slave" "-e" + "languageserver::run()")) (java-mode . eglot--eclipse-jdt-contact)) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE From f62f37d1ed8965eee954ad70794484bcc432de24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 19 Nov 2018 23:16:33 +0000 Subject: [PATCH 334/771] Fix potential security issue fontifying lsp doc Previously, a server could mistankely or maliciously call *-mode functions by in the response to a completion or hover request, specifically in the :documentation field of the response. Although there are plenty of similar avenues of attack in Emacs, it's probably a good idea not to let LSP servers decide which functions to call in an Emacs session running Eglot. * eglot.el (eglot--format-markup): Call major-mode to fontify buffer, not some dynamically constructed function name. (eglot-completion-at-point): Ensure eglot--format-markup runs in source buffer. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/154 --- lisp/progmodes/eglot.el | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c382c67e549..f4a02ac7a92 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -810,7 +810,7 @@ Doubles as an indicator of snippet support." (if (stringp markup) (list (string-trim markup) (intern "gfm-mode")) (list (plist-get markup :value) - (intern (concat (plist-get markup :language) "-mode" )))))) + major-mode)))) (with-temp-buffer (ignore-errors (funcall mode)) (insert string) (font-lock-ensure) (buffer-string)))) @@ -1585,11 +1585,13 @@ is not active." (get-text-property 0 'eglot--lsp-completion obj) :cancel-on-input t) - :documentation))))) - (when documentation + :documentation)))) + (formatted (and documentation + (eglot--format-markup documentation)))) + (when formatted (with-current-buffer (get-buffer-create " *eglot doc*") (erase-buffer) - (insert (eglot--format-markup documentation)) + (insert formatted) (current-buffer))))) :company-prefix-length (cl-some #'looking-back From fbb7d1e9183df446162f1365056a254e5ec4a29e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 22 Nov 2018 23:07:18 +0000 Subject: [PATCH 335/771] Correctly insert textedit-less snippets Fixes a slight regression from https://github.com/joaotavora/eglot/issues/160. * eglot.el (eglot-completion-at-point): When there is plain `insertText' snippet, delete the full completion text. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/167 --- lisp/progmodes/eglot.el | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f4a02ac7a92..9ff9cdf6f57 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1612,24 +1612,31 @@ is not active." bounds &allow-other-keys) (text-properties-at 0 comp) - (let ((fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (when (or fn textEdit) - ;; Undo the completion. If before completion the buffer was - ;; "foo.b" and now is "foo.bar", `comp' will be "bar". We - ;; want to delete only "ar" (`comp' minus the symbol whose - ;; bounds we've calculated before) (github#160). - (delete-region (+ (- (point) (length comp)) - (if bounds (- (cdr bounds) (car bounds)) 0)) - (point))) + (let ((snippet-fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) (cond (textEdit + ;; Undo the just the completed bit. If before + ;; completion the buffer was "foo.b" and now is + ;; "foo.bar", `comp' will be "bar". We want to + ;; delete only "ar" (`comp' minus the symbol + ;; whose bounds we've calculated before) + ;; (github#160). + (delete-region (+ (- (point) (length comp)) + (if bounds (- (cdr bounds) (car bounds)) 0)) + (point)) (cl-destructuring-bind (&key range newText) textEdit (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (delete-region beg end) (goto-char beg) - (funcall (or fn #'insert) newText))) + (funcall (or snippet-fn #'insert) newText))) (eglot--apply-text-edits additionalTextEdits)) - (fn (funcall fn insertText)))) + (snippet-fn + ;; A snippet should be inserted, but using plain + ;; `insertText'. This requires us to delete the + ;; whole completion, since `insertText' is the full + ;; completion's text. + (delete-region (- (point) (length comp)) (point)) + (funcall snippet-fn insertText)))) (eglot--signal-textDocument/didChange) (eglot-eldoc-function)))))))) From 95d48a3576400d2ffe4de1747f9a0794eb00f6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 23 Nov 2018 00:11:44 +0000 Subject: [PATCH 336/771] * eglot.el (eglot-completion-at-point): less chatter. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9ff9cdf6f57..b408e59f25d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1629,7 +1629,8 @@ is not active." (delete-region beg end) (goto-char beg) (funcall (or snippet-fn #'insert) newText))) - (eglot--apply-text-edits additionalTextEdits)) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) (snippet-fn ;; A snippet should be inserted, but using plain ;; `insertText'. This requires us to delete the From 333009a5c5e02dd23d6c7204e8b04f15ffff020c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 23 Nov 2018 00:12:45 +0000 Subject: [PATCH 337/771] * eglot.el (version): bump to 1.2 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b408e59f25d..365b5d21817 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.1 +;; Version: 1.2 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From d3fc3ce7e70df0a92931c31fcadb625d81b9f0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 23 Nov 2018 12:31:15 +0000 Subject: [PATCH 338/771] Control strictness towards incoming lsp messages A new variable, eglot-strict-mode controls whether Eglot is strict or lax with regard to incoming LSP messages. 1. Bug reports should be tested with eglot-strict-mode set to '(disallow-non-standard-keys enforce-required-keys) 2. Users struggling to get non-standard servers working set this variable to '(), nil. For now, by popular demand, this is the default value. Note that this commit in particular introduces a new infrastructure, but does not yet alter any code in Eglot to use it. Neither is the variable eglot--lsp-interface-alist populated. * eglot-tests.el (eglot-strict-interfaces): New test. * eglot.el (eglot--lsp-interface-alist): New variable. (eglot-strict-mode): New variable. (eglot--call-with-interface): New helper. (eglot--dbind): New macro. (eglot--lambda): New macro. GitHub-reference: per https://github.com/joaotavora/eglot/issues/144 GitHub-reference: per https://github.com/joaotavora/eglot/issues/156 --- lisp/progmodes/eglot.el | 88 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 365b5d21817..4996f5b639a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -197,6 +197,94 @@ let the buffer grow forever." (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") (17 . "File") (18 . "Reference"))) + + +;;; Message verification helpers +;;; +(defvar eglot--lsp-interface-alist `() + "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. + +INTERFACE-NAME is a symbol designated by the spec as \"export +interface\". INTERFACE is a list (REQUIRED OPTIONAL) where +REQUIRED and OPTIONAL are lists of keyword symbols designating +field names that must be, or may be, respectively, present in a +message adhering to that interface. + +Here's what an element of this alist might look like: + + (CreateFile . ((:kind :uri) (:options)))") + +(defvar eglot-strict-mode '() + "How strictly Eglot vetoes LSP messages from server. + +Value is a list of symbols: + +If a list containing the symbol `disallow-non-standard-keys', an +error is raised if any non-standard fields are sent by the +server. + +If the list containing the symbol `enforce-required-keys', an error +is raised if any required fields are missing from the message. + +If the list is empty, any non-standard fields sent by the server +and missing required fields are accepted (which may or may not +cause problems in Eglot's functioning later on).") + +(defun eglot--call-with-interface (interface object fn) + "Call FN, but first check that OBJECT conforms to INTERFACE. + +INTERFACE is a key to `eglot--lsp-interface-alist' and OBJECT is + a plist representing an LSP message." + (let* ((entry (assoc interface eglot--lsp-interface-alist)) + (required (car (cdr entry))) + (optional (cadr (cdr entry)))) + (when (memq 'enforce-required-keys eglot-strict-mode) + (cl-loop for req in required + when (eq 'eglot--not-present + (cl-getf object req 'eglot--not-present)) + collect req into missing + finally (when missing + (eglot--error + "A `%s' must have %s" interface missing)))) + (when (and entry (memq 'disallow-non-standard-keys eglot-strict-mode)) + (cl-loop + with allowed = (append required optional) + for (key _val) on object by #'cddr + unless (memq key allowed) collect key into disallowed + finally (when disallowed + (eglot--error + "A `%s' mustn't have %s" interface disallowed)))) + (funcall fn))) + +(cl-defmacro eglot--dbind (interface lambda-list object &body body) + "Destructure OBJECT of INTERFACE as CL-LAMBDA-LIST. +Honour `eglot-strict-mode'." + (declare (indent 3)) + (let ((fn-once `(lambda () ,@body)) + (lax-lambda-list (if (memq '&allow-other-keys lambda-list) + lambda-list + (append lambda-list '(&allow-other-keys)))) + (strict-lambda-list (delete '&allow-other-keys lambda-list))) + (if interface + `(cl-destructuring-bind ,lax-lambda-list ,object + (eglot--call-with-interface ',interface ,object ,fn-once)) + (let ((object-once (make-symbol "object-once"))) + `(let ((,object-once ,object)) + (if (memq 'disallow-non-standard-keys eglot-strict-mode) + (cl-destructuring-bind ,strict-lambda-list ,object-once + (funcall ,fn-once)) + (cl-destructuring-bind ,lax-lambda-list ,object-once + (funcall ,fn-once)))))))) + +(cl-defmacro eglot--lambda (interface cl-lambda-list &body body) + "Function of args CL-LAMBDA-LIST for processing INTERFACE objects. +Honour `eglot-strict-mode'." + (declare (indent 2)) + (let ((e (cl-gensym "jsonrpc-lambda-elem"))) + `(lambda (,e) + (eglot--dbind ,interface ,cl-lambda-list ,e + ,@body)))) + ;;; API (WORK-IN-PROGRESS!) ;;; From 1e7f94d75a30628be56ad00de8c6245f5f3e9fdf Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Fri, 23 Nov 2018 19:00:00 +0100 Subject: [PATCH 339/771] Codeaction command can be a command object () * eglot.el (eglot-code-actions): Handle case when the :command field is not a string. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/164 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/165 --- lisp/progmodes/eglot.el | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4996f5b639a..bc89cd28f37 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2011,7 +2011,12 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when edit (eglot--apply-workspace-edit edit)) (when command - (eglot-execute-command server (intern command) arguments))))) + (cond ((stringp command) + (eglot-execute-command server (intern command) arguments)) + ((listp command) + (eglot-execute-command server + (intern (plist-get command :command)) + (plist-get command :arguments)))))))) From 6ae6ce8b922bba07219bb8e10b67cc81a4703995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 23 Nov 2018 18:12:14 +0000 Subject: [PATCH 340/771] Revert "codeaction command can be a command object ()" This reverts commit 1e7f94d75a30628be56ad00de8c6245f5f3e9fdf. The spec doesn't define Command as implemented in the commit. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/164 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/165 --- lisp/progmodes/eglot.el | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bc89cd28f37..4996f5b639a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2011,12 +2011,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when edit (eglot--apply-workspace-edit edit)) (when command - (cond ((stringp command) - (eglot-execute-command server (intern command) arguments)) - ((listp command) - (eglot-execute-command server - (intern (plist-get command :command)) - (plist-get command :arguments)))))))) + (eglot-execute-command server (intern command) arguments))))) From 9e700ebc4c03254a832dce922a89b997b6f29354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 27 Nov 2018 12:42:56 +0000 Subject: [PATCH 341/771] Use entire line as xref summary when available After an original implementation by Michael Livshin. Also close https://github.com/joaotavora/eglot/issues/127. * eglot.el (eglot--xref-make): Rework. (xref-backend-definitions, xref-backend-references) (xref-backend-apropos): Simplify call to `eglot--xref-make'. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/52 --- lisp/progmodes/eglot.el | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4996f5b639a..8ba483b1676 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1478,13 +1478,31 @@ DUMMY is ignored." (advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) (advice-add 'xref-find-references :after #'eglot--xref-reset-known-symbols) -(defun eglot--xref-make (name uri position) - "Like `xref-make' but with LSP's NAME, URI and POSITION." - (cl-destructuring-bind (&key line character) position - (xref-make name (xref-make-file-location - (eglot--uri-to-path uri) - ;; F!@(#*&#$)CKING OFF-BY-ONE again - (1+ line) character)))) +(defun eglot--xref-make (name uri range) + "Like `xref-make' but with LSP's NAME, URI and RANGE. +Try to visit the target file for a richer summary line." + (pcase-let* + ((`(,beg . ,end) (eglot--range-region range)) + (file (eglot--uri-to-path uri)) + (visiting (find-buffer-visiting file)) + (collect (lambda () + (eglot--widening + (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) + (bol (progn (goto-char beg) (point-at-bol))) + (substring (buffer-substring bol (point-at-eol))) + (tab-width 1)) + (add-face-text-property (- beg bol) (- end bol) 'highlight + t substring) + (list substring (1+ (current-line)) (current-column)))))) + (`(,summary ,line ,column) + (cond + (visiting (with-current-buffer visiting (funcall collect))) + ((file-readable-p file) (with-temp-buffer (insert-file-contents file) + (funcall collect))) + (t ;; fall back to the "dumb strategy" + (let ((start (cl-getf range :start))) + (list name (1+ (cl-getf start :line)) (cl-getf start :character))))))) + (xref-make summary (xref-make-file-location file line column)))) (defun eglot--sort-xrefs (xrefs) (sort xrefs @@ -1537,7 +1555,7 @@ DUMMY is ignored." (if (vectorp definitions) definitions (vector definitions))))) (eglot--sort-xrefs (mapcar (jsonrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri range)) locations)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) @@ -1552,7 +1570,7 @@ DUMMY is ignored." (eglot--sort-xrefs (mapcar (jsonrpc-lambda (&key uri range) - (eglot--xref-make identifier uri (plist-get range :start))) + (eglot--xref-make identifier uri range)) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/references (append @@ -1566,7 +1584,7 @@ DUMMY is ignored." (mapcar (jsonrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location - (eglot--xref-make name uri (plist-get range :start)))) + (eglot--xref-make name uri range))) (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol `(:query ,pattern)))))) From e63203a8a70a59e5138740ec447d281b71ce7334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 27 Nov 2018 13:49:30 +0000 Subject: [PATCH 342/771] Improve performance of xref summary line collection * eglot.el (eglot--temp-location-buffers): New variable. (eglot--handling-xrefs): New macro. (eglot--xref-make): Use eglot--temp-location-buffers. (xref-backend-definitions, xref-backend-references) (xref-backend-apropos): Use eglot--handling-xrefs. GitHub-reference: per https://github.com/joaotavora/eglot/issues/52 GitHub-reference: per https://github.com/joaotavora/eglot/issues/127 --- lisp/progmodes/eglot.el | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8ba483b1676..15229352816 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1478,13 +1478,26 @@ DUMMY is ignored." (advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) (advice-add 'xref-find-references :after #'eglot--xref-reset-known-symbols) +(defvar eglot--temp-location-buffers (make-hash-table :test #'equal) + "Helper variable for `eglot--handling-xrefs'.") + +(defmacro eglot--handling-xrefs (&rest body) + "Properly sort and handle xrefs produced and returned by BODY." + `(unwind-protect + (sort (progn ,@body) + (lambda (a b) + (< (xref-location-line (xref-item-location a)) + (xref-location-line (xref-item-location b))))) + (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) + (clrhash eglot--temp-location-buffers))) + (defun eglot--xref-make (name uri range) "Like `xref-make' but with LSP's NAME, URI and RANGE. Try to visit the target file for a richer summary line." (pcase-let* - ((`(,beg . ,end) (eglot--range-region range)) - (file (eglot--uri-to-path uri)) - (visiting (find-buffer-visiting file)) + ((file (eglot--uri-to-path uri)) + (visiting (or (find-buffer-visiting file) + (gethash uri eglot--temp-location-buffers))) (collect (lambda () (eglot--widening (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) @@ -1497,19 +1510,16 @@ Try to visit the target file for a richer summary line." (`(,summary ,line ,column) (cond (visiting (with-current-buffer visiting (funcall collect))) - ((file-readable-p file) (with-temp-buffer (insert-file-contents file) - (funcall collect))) + ((file-readable-p file) (with-current-buffer + (puthash uri (generate-new-buffer " *temp*") + eglot--temp-location-buffers) + (insert-file-contents file) + (funcall collect))) (t ;; fall back to the "dumb strategy" (let ((start (cl-getf range :start))) (list name (1+ (cl-getf start :line)) (cl-getf start :character))))))) (xref-make summary (xref-make-file-location file line column)))) -(defun eglot--sort-xrefs (xrefs) - (sort xrefs - (lambda (a b) - (< (xref-location-line (xref-item-location a)) - (xref-location-line (xref-item-location b)))))) - (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (when (eglot--server-capable :documentSymbolProvider) (let ((server (eglot--current-server-or-lose)) @@ -1553,7 +1563,7 @@ Try to visit the target file for a richer summary line." (locations (and definitions (if (vectorp definitions) definitions (vector definitions))))) - (eglot--sort-xrefs + (eglot--handling-xrefs (mapcar (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri range)) locations)))) @@ -1567,7 +1577,7 @@ Try to visit the target file for a richer summary line." (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) (unless params (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--sort-xrefs + (eglot--handling-xrefs (mapcar (jsonrpc-lambda (&key uri range) (eglot--xref-make identifier uri range)) @@ -1580,7 +1590,7 @@ Try to visit the target file for a richer summary line." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (eglot--sort-xrefs + (eglot--handling-xrefs (mapcar (jsonrpc-lambda (&key name location &allow-other-keys) (cl-destructuring-bind (&key uri range) location From 1f3499320cca1f3c3ea2722bec5a8c2df003e2b7 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Tue, 27 Nov 2018 23:28:11 +0100 Subject: [PATCH 343/771] * eglot.el (eglot--current-column): new helper. (eglot-current-column-function): Set to eglot--current-column. (eglot--pos-to-lsp-position): Don't bind tab-width anymore. (eglot--xref-make): Use eglot--current-column. --- lisp/progmodes/eglot.el | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 15229352816..94de8d1bc48 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -811,7 +811,9 @@ CONNECT-ARGS are passed as additional arguments to (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) -(defvar eglot-current-column-function #'current-column +(defun eglot--current-column () (- (point) (point-at-bol))) + +(defvar eglot-current-column-function #'eglot--current-column "Function to calculate the current column. This is the inverse operation of @@ -833,8 +835,7 @@ for all others.") (eglot--widening (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE :character (progn (when pos (goto-char pos)) - (let ((tab-width 1)) - (funcall eglot-current-column-function)))))) + (funcall eglot-current-column-function))))) (defvar eglot-move-to-column-function #'move-to-column "Function to move to a column reported by the LSP server. @@ -1502,11 +1503,10 @@ Try to visit the target file for a richer summary line." (eglot--widening (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) (bol (progn (goto-char beg) (point-at-bol))) - (substring (buffer-substring bol (point-at-eol))) - (tab-width 1)) + (substring (buffer-substring bol (point-at-eol)))) (add-face-text-property (- beg bol) (- end bol) 'highlight t substring) - (list substring (1+ (current-line)) (current-column)))))) + (list substring (1+ (current-line)) (eglot--current-column)))))) (`(,summary ,line ,column) (cond (visiting (with-current-buffer visiting (funcall collect))) From ad2efe30748d60cfe1e89030a2c649b02ad1ae33 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Wed, 28 Nov 2018 19:53:35 +0100 Subject: [PATCH 344/771] Touch up last commit * eglot.el (eglot-current-column): Rename from eglot--current-column. (eglot-current-column-function): Use it as value and mention in docstring. (eglot--xref-make): Use eglot-current-column. --- lisp/progmodes/eglot.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 94de8d1bc48..2519189ca4d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -811,16 +811,16 @@ CONNECT-ARGS are passed as additional arguments to (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) -(defun eglot--current-column () (- (point) (point-at-bol))) +(defun eglot-current-column () (- (point) (point-at-bol))) -(defvar eglot-current-column-function #'eglot--current-column +(defvar eglot-current-column-function #'eglot-current-column "Function to calculate the current column. This is the inverse operation of `eglot-move-to-column-function' (which see). It is a function of no arguments returning a column number. For buffers managed by fully LSP-compliant servers, this should be set to -`eglot-lsp-abiding-column', and `current-column' (the default) +`eglot-lsp-abiding-column', and `eglot-current-column' (the default) for all others.") (defun eglot-lsp-abiding-column () @@ -1506,7 +1506,7 @@ Try to visit the target file for a richer summary line." (substring (buffer-substring bol (point-at-eol)))) (add-face-text-property (- beg bol) (- end bol) 'highlight t substring) - (list substring (1+ (current-line)) (eglot--current-column)))))) + (list substring (1+ (current-line)) (eglot-current-column)))))) (`(,summary ,line ,column) (cond (visiting (with-current-buffer visiting (funcall collect))) From bec802d0032054494911472b51b8bd421b0131df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 28 Nov 2018 20:26:37 +0000 Subject: [PATCH 345/771] Simplify interface of eglot--dbind macro * eglot.el (eglot--dbind): Use new interface. (eglot--lambda): Use new eglot--dbind interface. (eglot--lsp-interface-alist): Fix docstring. (eglot--call-with-interface): Simplify. (eglot--plist-keys): New helper. * eglot-tests.el (eglot-strict-interfaces): Add a new test clause. --- lisp/progmodes/eglot.el | 98 ++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2519189ca4d..594a638ad55 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -204,8 +204,8 @@ let the buffer grow forever." (defvar eglot--lsp-interface-alist `() "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. -INTERFACE-NAME is a symbol designated by the spec as \"export -interface\". INTERFACE is a list (REQUIRED OPTIONAL) where +INTERFACE-NAME is a symbol designated by the spec as +\"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where REQUIRED and OPTIONAL are lists of keyword symbols designating field names that must be, or may be, respectively, present in a message adhering to that interface. @@ -230,60 +230,56 @@ If the list is empty, any non-standard fields sent by the server and missing required fields are accepted (which may or may not cause problems in Eglot's functioning later on).") +(defun eglot--plist-keys (plist) + (cl-loop for (k _v) on plist by #'cddr collect k)) + (defun eglot--call-with-interface (interface object fn) - "Call FN, but first check that OBJECT conforms to INTERFACE. + "Call FN, checking that OBJECT conforms to INTERFACE." + (when-let ((missing (and (memq 'enforce-required-keys eglot-strict-mode) + (cl-set-difference (car (cdr interface)) + (eglot--plist-keys object))))) + (eglot--error "A `%s' must have %s" (car interface) missing)) + (when-let ((excess (and (memq 'disallow-non-standard-keys eglot-strict-mode) + (cl-set-difference + (eglot--plist-keys object) + (append (car (cdr interface)) (cadr (cdr interface))))))) + (eglot--error "A `%s' mustn't have %s" (car interface) excess)) + (funcall fn)) -INTERFACE is a key to `eglot--lsp-interface-alist' and OBJECT is - a plist representing an LSP message." - (let* ((entry (assoc interface eglot--lsp-interface-alist)) - (required (car (cdr entry))) - (optional (cadr (cdr entry)))) - (when (memq 'enforce-required-keys eglot-strict-mode) - (cl-loop for req in required - when (eq 'eglot--not-present - (cl-getf object req 'eglot--not-present)) - collect req into missing - finally (when missing - (eglot--error - "A `%s' must have %s" interface missing)))) - (when (and entry (memq 'disallow-non-standard-keys eglot-strict-mode)) - (cl-loop - with allowed = (append required optional) - for (key _val) on object by #'cddr - unless (memq key allowed) collect key into disallowed - finally (when disallowed - (eglot--error - "A `%s' mustn't have %s" interface disallowed)))) - (funcall fn))) - -(cl-defmacro eglot--dbind (interface lambda-list object &body body) - "Destructure OBJECT of INTERFACE as CL-LAMBDA-LIST. -Honour `eglot-strict-mode'." - (declare (indent 3)) - (let ((fn-once `(lambda () ,@body)) - (lax-lambda-list (if (memq '&allow-other-keys lambda-list) - lambda-list - (append lambda-list '(&allow-other-keys)))) - (strict-lambda-list (delete '&allow-other-keys lambda-list))) - (if interface - `(cl-destructuring-bind ,lax-lambda-list ,object - (eglot--call-with-interface ',interface ,object ,fn-once)) - (let ((object-once (make-symbol "object-once"))) - `(let ((,object-once ,object)) - (if (memq 'disallow-non-standard-keys eglot-strict-mode) - (cl-destructuring-bind ,strict-lambda-list ,object-once - (funcall ,fn-once)) - (cl-destructuring-bind ,lax-lambda-list ,object-once - (funcall ,fn-once)))))))) - -(cl-defmacro eglot--lambda (interface cl-lambda-list &body body) - "Function of args CL-LAMBDA-LIST for processing INTERFACE objects. +(cl-defmacro eglot--dbind (vars object &body body) + "Destructure OBJECT of binding VARS in BODY. +VARS is ([(INTERFACE)] SYMS...) Honour `eglot-strict-mode'." (declare (indent 2)) + (let ((interface-name (if (consp (car vars)) + (car (pop vars)))) + (object-once (make-symbol "object-once")) + (fn-once (make-symbol "fn-once"))) + (cond (interface-name + ;; jt@2018-11-29: maybe we check some things at compile + ;; time and use `byte-compiler-warn' here + `(let ((,object-once ,object)) + (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once + (eglot--call-with-interface (assoc ',interface-name + eglot--lsp-interface-alist) + ,object-once (lambda () + ,@body))))) + (t + `(let ((,object-once ,object) + (,fn-once (lambda (,@vars) ,@body))) + (if (memq 'disallow-non-standard-keys eglot-strict-mode) + (cl-destructuring-bind (&key ,@vars) ,object-once + (funcall ,fn-once ,@vars)) + (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once + (funcall ,fn-once ,@vars)))))))) + + +(cl-defmacro eglot--lambda (cl-lambda-list &body body) + "Function of args CL-LAMBDA-LIST for processing INTERFACE objects. +Honour `eglot-strict-mode'." + (declare (indent 1)) (let ((e (cl-gensym "jsonrpc-lambda-elem"))) - `(lambda (,e) - (eglot--dbind ,interface ,cl-lambda-list ,e - ,@body)))) + `(lambda (,e) (eglot--dbind ,cl-lambda-list ,e ,@body)))) ;;; API (WORK-IN-PROGRESS!) From 0bce2e3b2b3fe57cdbb63560241c65d406732252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 29 Nov 2018 22:36:03 +0000 Subject: [PATCH 346/771] Introduce eglot--dcase * eglot.el (eglot--dcase): New macro. * eglot-tests.el (eglot-dcase-with-interface) (eglot-dcase-no-interface): New tests. GitHub-reference: per https://github.com/joaotavora/eglot/issues/171 GitHub-reference: per https://github.com/joaotavora/eglot/issues/156 --- lisp/progmodes/eglot.el | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 594a638ad55..61f9b70a5c0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -281,6 +281,47 @@ Honour `eglot-strict-mode'." (let ((e (cl-gensym "jsonrpc-lambda-elem"))) `(lambda (,e) (eglot--dbind ,cl-lambda-list ,e ,@body)))) +(cl-defmacro eglot--dcase (obj &rest clauses) + "Like `pcase', but for the LSP object OBJ. +CLAUSES is a list (DESTRUCTURE FORMS...) where DESTRUCTURE is +treated as in `eglot-dbind'." + (let ((obj-once (make-symbol "obj-once"))) + `(let ((,obj-once ,obj)) + (cond + ,@(cl-loop + for (vars . body) in clauses + for vars-as-keywords = (mapcar (lambda (var) + (intern (format ":%s" var))) + vars) + for interface-name = (if (consp (car vars)) + (car (pop vars))) + for condition = + (if interface-name + `(let* ((interface + (or (assoc ',interface-name eglot--lsp-interface-alist) + (eglot--error "Unknown interface %s"))) + (object-keys (eglot--plist-keys ,obj-once)) + (required-keys (car (cdr interface)))) + (and (null (cl-set-difference required-keys object-keys)) + (or (null (memq 'disallow-non-standard-keys + eglot-strict-mode)) + (null (cl-set-difference + (cl-set-difference object-keys required-keys) + (cadr (cdr interface))))))) + ;; In this interface-less mode we don't check + ;; `eglot-strict-mode' at all. + `(null (cl-set-difference + ',vars-as-keywords + (eglot--plist-keys ,obj-once)))) + collect `(,condition + (cl-destructuring-bind (&key ,@vars &allow-other-keys) + ,obj-once + ,@body))) + (t + (eglot--error "%s didn't match any of %s" + ,obj-once + ',(mapcar #'car clauses))))))) + ;;; API (WORK-IN-PROGRESS!) ;;; From c515075fcb3d1bafed2dc881e059ba6bf1814f12 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Tue, 27 Nov 2018 23:42:45 +0100 Subject: [PATCH 347/771] Use eglot--dbind for destructuring * eglot.el (eglot--lsp-interface-alist): Add CodeAction, FileSystemWatcher, Registration, TextDocumentEdit, WorkspaceEdit. (eglot-handle-notification): Use eglot--dbind. (eglot--apply-workspace-edit): Use eglot--dbind and eglot--lambda. (eglot-code-actions): Use eglot--lambda. (eglot--register-workspace/didChangeWatchedFiles): Use eglot--lambda. --- lisp/progmodes/eglot.el | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 61f9b70a5c0..8d3977091d9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -201,7 +201,14 @@ let the buffer grow forever." ;;; Message verification helpers ;;; -(defvar eglot--lsp-interface-alist `() +(defvar eglot--lsp-interface-alist + `( + (CodeAction (:title) (:kind :diagnostics :edit :command)) + (FileSystemWatcher (:globPattern) (:kind)) + (Registration (:id :method) (:registerOptions)) + (TextDocumentEdit (:textDocument :edits) ()) + (WorkspaceEdit () (:changes :documentChanges)) + ) "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as @@ -1314,7 +1321,7 @@ COMMAND is a symbol naming the command." THINGS are either registrations or unregisterations." (cl-loop for thing in (cl-coerce things 'list) - collect (cl-destructuring-bind (&key id method registerOptions) thing + collect (eglot--dbind ((Registration) id method registerOptions) thing (apply (intern (format "eglot--%s-%s" how method)) server :id id registerOptions)) into results @@ -1990,9 +1997,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (defun eglot--apply-workspace-edit (wedit &optional confirm) "Apply the workspace edit WEDIT. If CONFIRM, ask user first." - (cl-destructuring-bind (&key changes documentChanges) wedit + (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit (let ((prepared - (mapcar (jsonrpc-lambda (&key textDocument edits) + (mapcar (eglot--lambda ((TextDocumentEdit) textDocument edits) (cl-destructuring-bind (&key uri version) textDocument (list (eglot--uri-to-path uri) edits version))) documentChanges)) @@ -2057,8 +2064,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) (menu-items - (or (mapcar (jsonrpc-lambda (&key title command arguments - edit _kind _diagnostics) + (or (mapcar (eglot--lambda ((CodeAction) title edit command arguments) `(,title . (:command ,command :arguments ,arguments :edit ,edit))) actions) @@ -2098,7 +2104,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Handle dynamic registration of workspace/didChangeWatchedFiles" (eglot--unregister-workspace/didChangeWatchedFiles server :id id) (let* (success - (globs (mapcar (lambda (w) (plist-get w :globPattern)) watchers))) + (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) + globPattern) + watchers))) (cl-labels ((handle-event (event) From 92ce9a30f1cfa8a974b9e40dec1126751aef1737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 1 Dec 2018 22:47:56 +0000 Subject: [PATCH 348/771] Don't break in indirect buffers Indirect buffers, such as the ones created by ediff-regions-wordwise, have eglot enabled but not buffer-file-name. Resort to the finding the file-name of the original buffer for these. * eglot.el (eglot--TextDocumentIdentifier): Work in indirect buffers. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/116 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/150 --- lisp/progmodes/eglot.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8d3977091d9..b240e0e860b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1345,7 +1345,10 @@ THINGS are either registrations or unregisterations." (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." - `(:uri ,(eglot--path-to-uri buffer-file-name))) + `(:uri ,(eglot--path-to-uri (or buffer-file-name + (ignore-errors + (buffer-file-name + (buffer-base-buffer))))))) (defvar-local eglot--versioned-identifier 0) From 949cf4e7a9725d414760dd02d463c3e67786b102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 2 Dec 2018 10:54:09 +0000 Subject: [PATCH 349/771] Support completioncontext to help servers like ccls * eglot.el (eglot-client-capabilities): Annouce textDocument/completion/contextSupport. (eglot--CompletionParams): New helper. (eglot-completion-at-point): Use it. GitHub-reference: close https://github.com/joaotavora/eglot/issues/173 --- lisp/progmodes/eglot.el | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b240e0e860b..ac10b434c7e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -374,7 +374,8 @@ treated as in `eglot-dbind'." `(:snippetSupport ,(if (eglot--snippet-expansion-fn) t - :json-false))) + :json-false)) + :contextSupport t) :hover `(:dynamicRegistration :json-false) :signatureHelp `(:dynamicRegistration :json-false) :references `(:dynamicRegistration :json-false) @@ -1374,6 +1375,19 @@ THINGS are either registrations or unregisterations." (list :textDocument (eglot--TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) +(defun eglot--CompletionParams () + (append + (eglot--TextDocumentPositionParams) + `(:context + ,(if-let (trigger (and (eq last-command 'self-insert-command) + (characterp last-input-event) + (cl-find last-input-event + (eglot--server-capable :completionProvider + :triggerCharacters) + :key (lambda (str) (aref str 0)) + :test #'char-equal))) + `(:triggerKind 2 :triggerCharacter ,trigger) `(:triggerKind 1))))) + (defvar-local eglot--recent-changes nil "Recent buffer changes as collected by `eglot--before-change'.") @@ -1693,7 +1707,7 @@ is not active." (lambda (_ignored) (let* ((resp (jsonrpc-request server :textDocument/completion - (eglot--TextDocumentPositionParams) + (eglot--CompletionParams) :deferred :textDocument/completion :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) From 1db7873cc111ce09d72093f6e76a1ccfea3afdec Mon Sep 17 00:00:00 2001 From: Mario Rodas Date: Sun, 2 Dec 2018 14:16:58 -0500 Subject: [PATCH 350/771] Use javascript-typescript-langserver for typescript-mode () Copyright-paperwork-exempt: Yes * eglot.el (eglot-server-programs): add typescript-mode to javascript-typescript-langserver's contact GitHub-reference: https://github.com/joaotavora/eglot/issues/174 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ac10b434c7e..2e99fc0cdd3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -82,7 +82,9 @@ (python-mode . ("pyls")) ((js-mode js2-mode - rjsx-mode) . ("javascript-typescript-stdio")) + rjsx-mode + typescript-mode) + . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) From 4a9914c0e66ebb2ba41e894a58d2598e8dc44687 Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Mon, 3 Dec 2018 12:49:34 +0100 Subject: [PATCH 351/771] Properly clear old diagnostics when making new ones * eglot.el (eglot-handle-notification textDocument/publishDiagnostics): Call flymake report function with :region. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/159 --- lisp/progmodes/eglot.el | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e99fc0cdd3..86db5ea322f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1313,7 +1313,13 @@ COMMAND is a symbol naming the command." message `((eglot-lsp-diag . ,diag-spec))))) into diags finally (cond ((and flymake-mode eglot--current-flymake-report-fn) - (funcall eglot--current-flymake-report-fn diags) + (funcall eglot--current-flymake-report-fn diags + ;; If the buffer hasn't changed since last + ;; call to the report function, flymake won't + ;; delete old diagnostics. Using :region + ;; keyword forces flymake to delete + ;; them (github#159). + :region (cons (point-min) (point-max))) (setq eglot--unreported-diagnostics nil)) (t (setq eglot--unreported-diagnostics (cons t diags)))))) From 3b9e5b1a84ada8769bdcd95f0d21674c7fc57714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 3 Dec 2018 12:24:26 +0000 Subject: [PATCH 352/771] Robustify previous fix against non-standard insertion bindings * eglot.el (eglot--managed-mode): Manage post-self-insert-hook. (eglot--last-inserted-char): New variable. (eglot--post-self-insert-hook): Set it. (eglot--before-change): Reset it. (eglot--CompletionParams): Use it. GitHub-reference: per https://github.com/joaotavora/eglot/issues/173 --- lisp/progmodes/eglot.el | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 86db5ea322f..e4547c51610 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1052,6 +1052,7 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) + (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) @@ -1068,6 +1069,7 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'xref-backend-functions 'eglot-xref-backend t) (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) + (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) @@ -1383,12 +1385,18 @@ THINGS are either registrations or unregisterations." (list :textDocument (eglot--TextDocumentIdentifier) :position (eglot--pos-to-lsp-position))) +(defvar-local eglot--last-inserted-char nil + "If non-nil, value of the last inserted character in buffer.") + +(defun eglot--post-self-insert-hook () + "Set `eglot--last-inserted-char.'" + (setq eglot--last-inserted-char last-input-event)) + (defun eglot--CompletionParams () (append (eglot--TextDocumentPositionParams) `(:context - ,(if-let (trigger (and (eq last-command 'self-insert-command) - (characterp last-input-event) + ,(if-let (trigger (and (characterp eglot--last-inserted-char) (cl-find last-input-event (eglot--server-capable :completionProvider :triggerCharacters) @@ -1406,15 +1414,18 @@ THINGS are either registrations or unregisterations." (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") (defun eglot--before-change (start end) - "Hook onto `before-change-functions'. -Records START and END, crucially convert them into -LSP (line/char) positions before that information is -lost (because the after-change thingy doesn't know if newlines -were deleted/added)" + "Hook onto `before-change-functions'." + ;; Records START and END, crucially convert them into LSP + ;; (line/char) positions before that information is lost (because + ;; the after-change thingy doesn't know if newlines were + ;; deleted/added) (when (listp eglot--recent-changes) (push `(,(eglot--pos-to-lsp-position start) ,(eglot--pos-to-lsp-position end)) - eglot--recent-changes))) + eglot--recent-changes)) + ;; Also, reset `eglot--last-inserted-char' which might be set later + ;; by `eglot--post-self-insert-hook'. + (setq eglot--last-inserted-char nil)) (defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. From b873654835710e7c51a0b2073467162800551bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 3 Dec 2018 14:07:21 +0000 Subject: [PATCH 353/771] Handle codeaction/command polymorphism with eglot--dcase * eglot-tests.el (eglot-dcase): Augment test. * eglot.el (eglot--lsp-interface-alist): Add Command interface. (eglot--dcase): Fix indentation. When given interface, always assume strict mode. (eglot-code-actions): Use eglot--dcase. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/164 --- lisp/progmodes/eglot.el | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e4547c51610..578a90d27ea 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -204,13 +204,12 @@ let the buffer grow forever." ;;; Message verification helpers ;;; (defvar eglot--lsp-interface-alist - `( - (CodeAction (:title) (:kind :diagnostics :edit :command)) + `((CodeAction (:title) (:kind :diagnostics :edit :command)) + (Command (:title :command) (:arguments)) (FileSystemWatcher (:globPattern) (:kind)) (Registration (:id :method) (:registerOptions)) (TextDocumentEdit (:textDocument :edits) ()) - (WorkspaceEdit () (:changes :documentChanges)) - ) + (WorkspaceEdit () (:changes :documentChanges))) "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as @@ -294,6 +293,7 @@ Honour `eglot-strict-mode'." "Like `pcase', but for the LSP object OBJ. CLAUSES is a list (DESTRUCTURE FORMS...) where DESTRUCTURE is treated as in `eglot-dbind'." + (declare (indent 1)) (let ((obj-once (make-symbol "obj-once"))) `(let ((,obj-once ,obj)) (cond @@ -306,19 +306,21 @@ treated as in `eglot-dbind'." (car (pop vars))) for condition = (if interface-name + ;; In this mode, we assume `eglot-strict-mode' is fully + ;; on, otherwise we can't disambiguate between certain + ;; types. `(let* ((interface (or (assoc ',interface-name eglot--lsp-interface-alist) (eglot--error "Unknown interface %s"))) (object-keys (eglot--plist-keys ,obj-once)) (required-keys (car (cdr interface)))) (and (null (cl-set-difference required-keys object-keys)) - (or (null (memq 'disallow-non-standard-keys - eglot-strict-mode)) - (null (cl-set-difference - (cl-set-difference object-keys required-keys) - (cadr (cdr interface))))))) + (null (cl-set-difference + (cl-set-difference object-keys required-keys) + (cadr (cdr interface)))))) ;; In this interface-less mode we don't check - ;; `eglot-strict-mode' at all. + ;; `eglot-strict-mode' at all: just check that the object + ;; has all the keys the user wants to destructure. `(null (cl-set-difference ',vars-as-keywords (eglot--plist-keys ,obj-once)))) @@ -2100,9 +2102,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--diag-data diag)))) (flymake-diagnostics beg end))])))) (menu-items - (or (mapcar (eglot--lambda ((CodeAction) title edit command arguments) - `(,title . (:command ,command :arguments ,arguments - :edit ,edit))) + (or (mapcar (jsonrpc-lambda (&rest all &key title &allow-other-keys) + (cons title all)) actions) (eglot--error "No code actions here"))) (menu `("Eglot code actions:" ("dummy" ,@menu-items))) @@ -2114,11 +2115,14 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eq (setq retval (tmm-prompt menu)) never-mind) (keyboard-quit) retval))))) - (cl-destructuring-bind (&key _title command arguments edit) action - (when edit - (eglot--apply-workspace-edit edit)) - (when command - (eglot-execute-command server (intern command) arguments))))) + (eglot--dcase action + (((Command) command arguments) + (eglot-execute-command server (intern command) arguments)) + (((CodeAction) edit command) + (when edit (eglot--apply-workspace-edit edit)) + (when command + (eglot--dbind ((Command) command arguments) command + (eglot-execute-command server (intern command) arguments))))))) From f2326f4e13ecf7c244b31aa727cec6981056ace2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 4 Dec 2018 09:47:36 +0000 Subject: [PATCH 354/771] Fix bug introduced by previous fix * eglot.el (eglot--CompletionParams): Don't use last-input-event. GitHub-reference: per https://github.com/joaotavora/eglot/issues/173 --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 578a90d27ea..3bb93dcc4ff 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1399,7 +1399,7 @@ THINGS are either registrations or unregisterations." (eglot--TextDocumentPositionParams) `(:context ,(if-let (trigger (and (characterp eglot--last-inserted-char) - (cl-find last-input-event + (cl-find eglot--last-inserted-char (eglot--server-capable :completionProvider :triggerCharacters) :key (lambda (str) (aref str 0)) @@ -2116,8 +2116,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (keyboard-quit) retval))))) (eglot--dcase action - (((Command) command arguments) - (eglot-execute-command server (intern command) arguments)) + (((Command) command arguments) + (eglot-execute-command server (intern command) arguments)) (((CodeAction) edit command) (when edit (eglot--apply-workspace-edit edit)) (when command From 0918c9d1a585f8c86bdd13c39704240b3ab81fc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 4 Dec 2018 23:10:35 +0000 Subject: [PATCH 355/771] Adjust previous fix * eglot.el (eglot--before-change): Don't reset eglot--last-inserted-char here. (eglot--pre-command-hook): Do it here. (eglot--managed-mode): Add/remove eglot--pre-command-hook to/from pre-command-hook. GitHub-reference: per https://github.com/joaotavora/eglot/issues/173 --- lisp/progmodes/eglot.el | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3bb93dcc4ff..727b76166dd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1055,6 +1055,7 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff 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) (add-function :before-until (local 'eldoc-documentation-function) #'eglot-eldoc-function) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) @@ -1072,6 +1073,7 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) + (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) (remove-function (local 'eldoc-documentation-function) #'eglot-eldoc-function) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) @@ -1394,6 +1396,10 @@ THINGS are either registrations or unregisterations." "Set `eglot--last-inserted-char.'" (setq eglot--last-inserted-char last-input-event)) +(defun eglot--pre-command-hook () + "Reset `eglot--last-inserted-char.'" + (setq eglot--last-inserted-char nil)) + (defun eglot--CompletionParams () (append (eglot--TextDocumentPositionParams) @@ -1424,10 +1430,7 @@ THINGS are either registrations or unregisterations." (when (listp eglot--recent-changes) (push `(,(eglot--pos-to-lsp-position start) ,(eglot--pos-to-lsp-position end)) - eglot--recent-changes)) - ;; Also, reset `eglot--last-inserted-char' which might be set later - ;; by `eglot--post-self-insert-hook'. - (setq eglot--last-inserted-char nil)) + eglot--recent-changes))) (defun eglot--after-change (start end pre-change-length) "Hook onto `after-change-functions'. From 30ab4e3eedd0ca08e7d4d8ad5c303f58c40e9228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 5 Dec 2018 19:54:55 +0000 Subject: [PATCH 356/771] Use eglot--dbind and eglot--lambda throughout The default behaviour of these macros is to be lenient towards servers sending unknown keys, which should fix the issue. * eglot.el (eglot--lsp-interface-alist): Add a bunch of new interfaces. (eglot--connect, eglot-handle-notification) (xref-backend-identifier-completion-table) (xref-backend-definitions, xref-backend-apropos) (xref-backend-references, eglot-completion-at-point) (eglot--sig-info, eglot-help-at-point, eglot-eldoc-function) (eglot-imenu, eglot--apply-text-edits) (eglot--apply-workspace-edit) (eglot--register-workspace/didChangeWatchedFiles): Use eglot--dbind and eglot--lambda to destructure LSP objects. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/144 --- lisp/progmodes/eglot.el | 171 +++++++++++++++++++++++----------------- 1 file changed, 100 insertions(+), 71 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 727b76166dd..ba61de9315d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -208,8 +208,34 @@ let the buffer grow forever." (Command (:title :command) (:arguments)) (FileSystemWatcher (:globPattern) (:kind)) (Registration (:id :method) (:registerOptions)) + (Hover (:contents) (:range)) + (SymbolInformation + (:name :kind :location) + (:deprecated :containerName)) + (Position (:line :character)) + (Range (:start :end)) + (Location (:uri :range)) + (Diagnostic (:range :message) + (:severity :code :source :relatedInformation)) + (TextEdit (:range :newText)) (TextDocumentEdit (:textDocument :edits) ()) - (WorkspaceEdit () (:changes :documentChanges))) + (VersionedTextDocumentIdentifier (:uri :version) ()) + (WorkspaceEdit () (:changes :documentChanges)) + (MarkupContent (:kind :value)) + (InitializeResult (:capabilities)) + (ShowMessageParams (:type :message)) + (ShowMessageRequestParams (:type :message) (:actions)) + (LogMessageParams (:type :message)) + (Registration (:id :method) (:registerOptions)) + (CompletionItem + (:label ) + (:kind :detail :documentation :deprecated :preselect :sortText :filterText + :insertText :insertTextFormat :textEdit :additionalTextEdits? + :commitCharacters :command :data)) + (SignatureHelp (:signatures) (:activeSignature :activeParameter)) + (SignatureInformation (:label) (:documentation :parameters)) + (ParameterInformation (:label) (:documentation)) + (DocumentHighlight) (:range) (:kind)) "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as @@ -741,7 +767,7 @@ This docstring appeases checkdoc, that's all." server) :capabilities (eglot-client-capabilities server)) :success-fn - (jsonrpc-lambda (&key capabilities) + (eglot--lambda ((InitializeResult) capabilities) (unless cancelled (push server (gethash project eglot--servers-by-project)) @@ -769,7 +795,7 @@ in project `%s'." (eglot--project-nickname server)) (when tag (throw tag t)))) :timeout eglot-connect-timeout - :error-fn (jsonrpc-lambda (&key code message _data) + :error-fn (eglot--lambda ((ResponseError) code message) (unless cancelled (jsonrpc-shutdown server) (let ((msg (format "%s: %s" code message))) @@ -1288,13 +1314,12 @@ COMMAND is a symbol naming the command." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (cl-destructuring-bind (&key range ((:severity sev)) _group - _code source message - &allow-other-keys) + collect (eglot--dbind ((Diagnostic) range message severity source) diag-spec (setq message (concat source ": " message)) (pcase-let - ((`(,beg . ,end) (eglot--range-region range))) + ((sev severity) + (`(,beg . ,end) (eglot--range-region range))) ;; Fallback to `flymake-diag-region' if server ;; botched the range (when (= beg end) @@ -1613,8 +1638,8 @@ Try to visit the target file for a richer summary line." (lambda (string) (setq eglot--xref-known-symbols (mapcar - (jsonrpc-lambda - (&key name kind location containerName _deprecated) + (eglot--lambda + ((SymbolInformation) name kind location containerName) (propertize name :textDocumentPositionParams (list :textDocument text-id @@ -1649,7 +1674,7 @@ Try to visit the target file for a richer summary line." (and definitions (if (vectorp definitions) definitions (vector definitions))))) (eglot--handling-xrefs - (mapcar (jsonrpc-lambda (&key uri range) + (mapcar (eglot--lambda ((Location) uri range) (eglot--xref-make identifier uri range)) locations)))) @@ -1664,7 +1689,7 @@ Try to visit the target file for a richer summary line." (eglot--error "Don' know where %s is in the workspace!" identifier)) (eglot--handling-xrefs (mapcar - (jsonrpc-lambda (&key uri range) + (eglot--lambda ((Location) uri range) (eglot--xref-make identifier uri range)) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/references @@ -1677,8 +1702,8 @@ Try to visit the target file for a richer summary line." (when (eglot--server-capable :workspaceSymbolProvider) (eglot--handling-xrefs (mapcar - (jsonrpc-lambda (&key name location &allow-other-keys) - (cl-destructuring-bind (&key uri range) location + (eglot--lambda ((SymbolInformation) name location) + (eglot--dbind ((Location) uri range) location (eglot--xref-make name uri range))) (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol @@ -1746,16 +1771,15 @@ is not active." (string-trim-left label)) (t (or insertText (string-trim-left label)))))) - (setq all (append all `(:bounds ,bounds))) (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--completion-bounds bounds completion) (put-text-property 0 1 'eglot--lsp-completion all completion) completion)) items))))) :annotation-function (lambda (obj) - (cl-destructuring-bind (&key detail kind insertTextFormat - &allow-other-keys) - (text-properties-at 0 obj) + (eglot--dbind ((CompletionItem) detail kind insertTextFormat) + (get-text-property 0 'eglot--lsp-completion obj) (let* ((detail (and (stringp detail) (not (string= detail "")) detail)) @@ -1806,15 +1830,18 @@ is not active." ;; buffer, `comp' won't have any properties. A ;; lookup should fix that (github#148) (cl-find comp strings :test #'string=)))) - (cl-destructuring-bind (&key insertTextFormat - insertText - textEdit - additionalTextEdits - bounds - &allow-other-keys) - (text-properties-at 0 comp) + (eglot--dbind ((CompletionItem) insertTextFormat + insertText + textEdit + additionalTextEdits) + (get-text-property 0 'eglot--lsp-completion comp) (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) + (eglot--snippet-expansion-fn))) + ;; FIXME: it would have been much easier to fetch + ;; these from the lexical environment, but we can't + ;; in company because of + ;; https://github.com/company-mode/company-mode/pull/845 + (bounds (get-text-property 0 'eglot--completion-bounds comp))) (cond (textEdit ;; Undo the just the completed bit. If before ;; completion the buffer was "foo.b" and now is @@ -1825,7 +1852,7 @@ is not active." (delete-region (+ (- (point) (length comp)) (if bounds (- (cdr bounds) (car bounds)) 0)) (point)) - (cl-destructuring-bind (&key range newText) textEdit + (eglot--dbind ((TextEdit) range newText) textEdit (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (delete-region beg end) (goto-char beg) @@ -1854,47 +1881,48 @@ is not active." (defun eglot--sig-info (sigs active-sig active-param) (cl-loop for (sig . moresigs) on (append sigs nil) for i from 0 - concat (cl-destructuring-bind (&key label documentation parameters) sig - (with-temp-buffer - (save-excursion (insert label)) - (when (looking-at "\\([^(]+\\)(") - (add-face-text-property (match-beginning 1) (match-end 1) - 'font-lock-function-name-face)) + concat + (eglot--dbind ((SignatureInformation) label documentation parameters) sig + (with-temp-buffer + (save-excursion (insert label)) + (when (looking-at "\\([^(]+\\)(") + (add-face-text-property (match-beginning 1) (match-end 1) + 'font-lock-function-name-face)) - (when (and (stringp documentation) (eql i active-sig) - (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" - documentation)) - (setq documentation (match-string 1 documentation)) - (unless (string-prefix-p (string-trim documentation) label) - (goto-char (point-max)) - (insert ": " documentation))) - (when (and (eql i active-sig) active-param - (< -1 active-param (length parameters))) - (cl-destructuring-bind (&key label documentation) - (aref parameters active-param) - (goto-char (point-min)) - (let ((case-fold-search nil)) - (cl-loop for nmatches from 0 - while (and (not (string-empty-p label)) - (search-forward label nil t)) - finally do - (when (= 1 nmatches) - (add-face-text-property - (- (point) (length label)) (point) - 'eldoc-highlight-function-argument)))) - (when documentation - (goto-char (point-max)) - (insert "\n" - (propertize - label 'face 'eldoc-highlight-function-argument) - ": " (eglot--format-markup documentation))))) - (buffer-string))) + (when (and (stringp documentation) (eql i active-sig) + (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" + documentation)) + (setq documentation (match-string 1 documentation)) + (unless (string-prefix-p (string-trim documentation) label) + (goto-char (point-max)) + (insert ": " documentation))) + (when (and (eql i active-sig) active-param + (< -1 active-param (length parameters))) + (eglot--dbind ((ParameterInformation) label documentation) + (aref parameters active-param) + (goto-char (point-min)) + (let ((case-fold-search nil)) + (cl-loop for nmatches from 0 + while (and (not (string-empty-p label)) + (search-forward label nil t)) + finally do + (when (= 1 nmatches) + (add-face-text-property + (- (point) (length label)) (point) + 'eldoc-highlight-function-argument)))) + (when documentation + (goto-char (point-max)) + (insert "\n" + (propertize + label 'face 'eldoc-highlight-function-argument) + ": " (eglot--format-markup documentation))))) + (buffer-string))) when moresigs concat "\n")) (defun eglot-help-at-point () "Request \"hover\" information for the thing at point." (interactive) - (cl-destructuring-bind (&key contents range) + (eglot--dbind ((Hover) contents range) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) @@ -1917,8 +1945,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (jsonrpc-async-request server :textDocument/signatureHelp position-params :success-fn - (jsonrpc-lambda (&key signatures activeSignature - activeParameter) + (eglot--lambda ((SignatureHelp) + signatures activeSignature activeParameter) (when-buffer-window (when (cl-plusp (length signatures)) (setq sig-showing t) @@ -1929,7 +1957,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when (eglot--server-capable :hoverProvider) (jsonrpc-async-request server :textDocument/hover position-params - :success-fn (jsonrpc-lambda (&key contents range) + :success-fn (eglot--lambda ((Hover) contents range) (unless sig-showing (when-buffer-window (when-let (info (and contents @@ -1946,7 +1974,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (setq eglot--highlights (when-buffer-window (mapcar - (jsonrpc-lambda (&key range _kind _role) + (eglot--lambda ((DocumentHighlight) range) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) @@ -1962,8 +1990,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (if (eglot--server-capable :documentSymbolProvider) (let ((entries (mapcar - (jsonrpc-lambda - (&key name kind location containerName _deprecated) + (eglot--lambda + ((SymbolInformation) name kind location containerName) (cons (propertize name :kind (alist-get kind eglot--symbol-kind-names @@ -2030,7 +2058,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." beg (+ beg (length newText)) length)))) (progress-reporter-update reporter (cl-incf done))))))) - (mapcar (jsonrpc-lambda (&key range newText) + (mapcar (eglot--lambda ((TextEdit) range newText) (cons newText (eglot--range-region range 'markers))) (reverse edits))) (undo-amalgamate-change-group change-group) @@ -2041,7 +2069,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit (let ((prepared (mapcar (eglot--lambda ((TextDocumentEdit) textDocument edits) - (cl-destructuring-bind (&key uri version) textDocument + (eglot--dbind ((VersionedTextDocumentIdentifier) uri version) + textDocument (list (eglot--uri-to-path uri) edits version))) documentChanges)) edit) @@ -2055,7 +2084,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (mapconcat #'identity (mapcar #'car prepared) "\n "))) (eglot--error "User cancelled server edit"))) (while (setq edit (car prepared)) - (cl-destructuring-bind (path edits &optional version) edit + (pcase-let ((`(,path ,edits ,version) edit)) (with-current-buffer (find-file-noselect path) (eglot--apply-text-edits edits version)) (pop prepared)) @@ -2153,7 +2182,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (cl-labels ((handle-event (event) - (cl-destructuring-bind (desc action file &optional file1) event + (pcase-let ((`(,desc ,action ,file ,file1) event)) (cond ((and (memq action '(created changed deleted)) (cl-find file globs From 60f45f0f30ae5ba13c913be25166baff30ccba5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 6 Dec 2018 18:26:17 +0000 Subject: [PATCH 357/771] Warn about suspicious interface usage at compile-time For fun, set eglot-strict-mode to '(disallow-non-standard-keys enforce-required-keys enforce-optional-keys) when compiling, or just use flymake-mode in eglot.el. * eglot.el (eglot--lsp-interface-alist): Use in compile-time. Order alphabetically. Fix a few bugs. (eglot-strict-mode): Disallow non-standard-keys when compiling. Update docstring. (eglot--keywordize-vars, eglot--check-interface): New compile-time-helpers. (eglot--dbind, eglot--dcase): Use new helpers. --- lisp/progmodes/eglot.el | 189 ++++++++++++++++++++++++++-------------- 1 file changed, 122 insertions(+), 67 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ba61de9315d..0286f75869d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -203,40 +203,41 @@ let the buffer grow forever." ;;; Message verification helpers ;;; -(defvar eglot--lsp-interface-alist - `((CodeAction (:title) (:kind :diagnostics :edit :command)) - (Command (:title :command) (:arguments)) - (FileSystemWatcher (:globPattern) (:kind)) - (Registration (:id :method) (:registerOptions)) - (Hover (:contents) (:range)) - (SymbolInformation - (:name :kind :location) - (:deprecated :containerName)) - (Position (:line :character)) - (Range (:start :end)) - (Location (:uri :range)) - (Diagnostic (:range :message) - (:severity :code :source :relatedInformation)) - (TextEdit (:range :newText)) - (TextDocumentEdit (:textDocument :edits) ()) - (VersionedTextDocumentIdentifier (:uri :version) ()) - (WorkspaceEdit () (:changes :documentChanges)) - (MarkupContent (:kind :value)) - (InitializeResult (:capabilities)) - (ShowMessageParams (:type :message)) - (ShowMessageRequestParams (:type :message) (:actions)) - (LogMessageParams (:type :message)) - (Registration (:id :method) (:registerOptions)) - (CompletionItem - (:label ) - (:kind :detail :documentation :deprecated :preselect :sortText :filterText - :insertText :insertTextFormat :textEdit :additionalTextEdits? - :commitCharacters :command :data)) - (SignatureHelp (:signatures) (:activeSignature :activeParameter)) - (SignatureInformation (:label) (:documentation :parameters)) - (ParameterInformation (:label) (:documentation)) - (DocumentHighlight) (:range) (:kind)) - "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. +(eval-and-compile + (defvar eglot--lsp-interface-alist + `( + (CodeAction (:title) (:kind :diagnostics :edit :command)) + (Command (:title :command) (:arguments)) + (CompletionItem (:label) + (:kind :detail :documentation :deprecated :preselect + :sortText :filterText :insertText :insertTextFormat + :textEdit :additionalTextEdits :commitCharacters + :command :data)) + (Diagnostic (:range :message) (:severity :code :source :relatedInformation)) + (DocumentHighlight (:range) (:kind)) + (FileSystemWatcher (:globPattern) (:kind)) + (Hover (:contents) (:range)) + (InitializeResult (:capabilities)) + (Location (:uri :range)) + (LogMessageParams (:type :message)) + (MarkupContent (:kind :value)) + (ParameterInformation (:label) (:documentation)) + (Position (:line :character)) + (Range (:start :end)) + (Registration (:id :method) (:registerOptions)) + (Registration (:id :method) (:registerOptions)) + (ResponseError (:code :message) (:data)) + (ShowMessageParams (:type :message)) + (ShowMessageRequestParams (:type :message) (:actions)) + (SignatureHelp (:signatures) (:activeSignature :activeParameter)) + (SignatureInformation (:label) (:documentation :parameters)) + (SymbolInformation (:name :kind :location) (:deprecated :containerName)) + (TextDocumentEdit (:textDocument :edits) ()) + (TextEdit (:range :newText)) + (VersionedTextDocumentIdentifier (:uri :version) ()) + (WorkspaceEdit () (:changes :documentChanges)) + ) + "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as \"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where @@ -246,23 +247,38 @@ message adhering to that interface. Here's what an element of this alist might look like: - (CreateFile . ((:kind :uri) (:options)))") + (CreateFile . ((:kind :uri) (:options)))")) -(defvar eglot-strict-mode '() - "How strictly Eglot vetoes LSP messages from server. +(eval-and-compile + (defvar eglot-strict-mode (if load-file-name '() + '(disallow-non-standard-keys + ;; Uncomment these two for fun at + ;; compile-time or with flymake-mode. + ;; + ;; enforce-required-keys + ;; enforce-optional-keys + )) + "How strictly to check LSP interfaces at compile- and run-time. Value is a list of symbols: -If a list containing the symbol `disallow-non-standard-keys', an -error is raised if any non-standard fields are sent by the -server. +If the symbol `disallow-non-standard-keys' is present, an error +is raised if any extraneous fields are sent by the server. At +compile-time, a warning is raised if a destructuring spec +includes such a field. -If the list containing the symbol `enforce-required-keys', an error -is raised if any required fields are missing from the message. +If the symbol `enforce-required-keys' is present, an error is +raised if any required fields are missing from the message sent +from the server. At compile-time, a warning is raised if a +destructuring spec doesn't use such a field. + +If the symbol `enforce-optional-keys' is present, nothing special +happens at run-time. At compile-time, a warning is raised if a +destructuring spec doesn't use all optional fields. If the list is empty, any non-standard fields sent by the server and missing required fields are accepted (which may or may not -cause problems in Eglot's functioning later on).") +cause problems in Eglot's functioning later on).")) (defun eglot--plist-keys (plist) (cl-loop for (k _v) on plist by #'cddr collect k)) @@ -280,6 +296,45 @@ cause problems in Eglot's functioning later on).") (eglot--error "A `%s' mustn't have %s" (car interface) excess)) (funcall fn)) +(eval-and-compile + (defun eglot--keywordize-vars (vars) + (mapcar (lambda (var) (intern (format ":%s" var))) vars)) + + (defun eglot--check-interface (interface-name vars) + (let ((interface + (assoc interface-name eglot--lsp-interface-alist))) + (cond (interface + (let ((too-many + (and + (memq 'disallow-non-standard-keys eglot-strict-mode) + (cl-set-difference + (eglot--keywordize-vars vars) + (append (car (cdr interface)) + (cadr (cdr interface)))))) + (ignored-required + (and + (memq 'enforce-required-keys eglot-strict-mode) + (cl-set-difference + (car (cdr interface)) + (eglot--keywordize-vars vars)))) + (missing-out + (and + (memq 'enforce-optional-keys eglot-strict-mode) + (cl-set-difference + (cadr (cdr interface)) + (eglot--keywordize-vars vars))))) + (when too-many (byte-compile-warn + "Destructuring for %s has extraneous %s" + interface-name too-many)) + (when ignored-required (byte-compile-warn + "Destructuring for %s ignores required %s" + interface-name ignored-required)) + (when missing-out (byte-compile-warn + "Destructuring for %s is missing out on %s" + interface-name missing-out)))) + (t + (byte-compile-warn "Unknown LSP interface %s" interface-name)))))) + (cl-defmacro eglot--dbind (vars object &body body) "Destructure OBJECT of binding VARS in BODY. VARS is ([(INTERFACE)] SYMS...) @@ -290,8 +345,7 @@ Honour `eglot-strict-mode'." (object-once (make-symbol "object-once")) (fn-once (make-symbol "fn-once"))) (cond (interface-name - ;; jt@2018-11-29: maybe we check some things at compile - ;; time and use `byte-compiler-warn' here + (eglot--check-interface interface-name vars) `(let ((,object-once ,object)) (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once (eglot--call-with-interface (assoc ',interface-name @@ -325,31 +379,32 @@ treated as in `eglot-dbind'." (cond ,@(cl-loop for (vars . body) in clauses - for vars-as-keywords = (mapcar (lambda (var) - (intern (format ":%s" var))) - vars) + for vars-as-keywords = (eglot--keywordize-vars vars) for interface-name = (if (consp (car vars)) (car (pop vars))) for condition = - (if interface-name - ;; In this mode, we assume `eglot-strict-mode' is fully - ;; on, otherwise we can't disambiguate between certain - ;; types. - `(let* ((interface - (or (assoc ',interface-name eglot--lsp-interface-alist) - (eglot--error "Unknown interface %s"))) - (object-keys (eglot--plist-keys ,obj-once)) - (required-keys (car (cdr interface)))) - (and (null (cl-set-difference required-keys object-keys)) - (null (cl-set-difference - (cl-set-difference object-keys required-keys) - (cadr (cdr interface)))))) - ;; In this interface-less mode we don't check - ;; `eglot-strict-mode' at all: just check that the object - ;; has all the keys the user wants to destructure. - `(null (cl-set-difference - ',vars-as-keywords - (eglot--plist-keys ,obj-once)))) + (cond (interface-name + (eglot--check-interface interface-name vars) + ;; In this mode, in runtime, we assume + ;; `eglot-strict-mode' is fully on, otherwise we + ;; can't disambiguate between certain types. + `(let* ((interface + (or (assoc ',interface-name eglot--lsp-interface-alist) + (eglot--error "Unknown LSP interface %s" + ',interface-name))) + (object-keys (eglot--plist-keys ,obj-once)) + (required-keys (car (cdr interface)))) + (and (null (cl-set-difference required-keys object-keys)) + (null (cl-set-difference + (cl-set-difference object-keys required-keys) + (cadr (cdr interface))))))) + (t + ;; In this interface-less mode we don't check + ;; `eglot-strict-mode' at all: just check that the object + ;; has all the keys the user wants to destructure. + `(null (cl-set-difference + ',vars-as-keywords + (eglot--plist-keys ,obj-once))))) collect `(,condition (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,obj-once From dfd413c22d235b37f1ad25127b98b5827d2cb1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 7 Dec 2018 14:46:23 +0000 Subject: [PATCH 358/771] Scratch/use elpa flymake () Use GNU ELPA's Flymake * eglot.el (Package-Requires): require Flymake 1.0.2. (version< emacs-version "27.0"): Remove horrible hack * Makefile (ELPADEPS): Renamed from JSONRPC. (%.elc): Use it. (eglot-check): Use it. GitHub-reference: https://github.com/joaotavora/eglot/issues/178 --- lisp/progmodes/eglot.el | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0286f75869d..45529307d69 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.6")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.6") (flymake "1.0.2")) ;; 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 @@ -2400,40 +2400,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Eclipse JDT breaks spec and replies with edits as arguments." (mapc #'eglot--apply-workspace-edit arguments)) - -;; FIXME: A horrible hack of Flymake's insufficient API that must go -;; into Emacs master, or better, 26.2 -(when (version< emacs-version "27.0") - (cl-defstruct (eglot--diag (:include flymake--diag) - (:constructor eglot--make-diag-1)) - data-1) - (defsubst eglot--make-diag (buffer beg end type text data) - (let ((sym (alist-get type eglot--diag-error-types-to-old-types))) - (eglot--make-diag-1 :buffer buffer :beg beg :end end :type sym - :text text :data-1 data))) - (defsubst eglot--diag-data (diag) - (and (eglot--diag-p diag) (eglot--diag-data-1 diag))) - (defvar eglot--diag-error-types-to-old-types - '((eglot-error . :error) - (eglot-warning . :warning) - (eglot-note . :note))) - (advice-add - 'flymake--highlight-line :after - (lambda (diag) - (when (eglot--diag-p diag) - (let ((ov (cl-find diag - (overlays-at (flymake-diagnostic-beg diag)) - :key (lambda (ov) - (overlay-get ov 'flymake-diagnostic)))) - (overlay-properties - (get (car (rassoc (flymake-diagnostic-type diag) - eglot--diag-error-types-to-old-types)) - 'flymake-overlay-control))) - (cl-loop for (k . v) in overlay-properties - do (overlay-put ov k v))))) - '((name . eglot-hacking-in-some-per-diag-overlay-properties)))) - - (provide 'eglot) ;;; eglot.el ends here From 6d3310d83ca568daf4092b9118b872feef5d12b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 7 Dec 2018 22:43:13 +0000 Subject: [PATCH 359/771] Handle array params to server notification or requests * eglot.el (eglot-handle-notification): Remove extraneous id (eglot--connect): If params is an array, make it a list. --- lisp/progmodes/eglot.el | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 45529307d69..05971e13cd2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -429,7 +429,7 @@ treated as in `eglot-dbind'." (cl-defgeneric eglot-handle-request (server method &rest params) "Handle SERVER's METHOD request with PARAMS.") -(cl-defgeneric eglot-handle-notification (server method id &rest params) +(cl-defgeneric eglot-handle-notification (server method &rest params) "Handle SERVER's METHOD notification with PARAMS.") (cl-defgeneric eglot-execute-command (server command arguments) @@ -783,10 +783,8 @@ This docstring appeases checkdoc, that's all." :noquery t :stderr (get-buffer-create (format "*%s stderr*" readable-name)))))))) - (spread - (lambda (fn) - (lambda (&rest args) - (apply fn (append (butlast args) (car (last args))))))) + (spread (lambda (fn) (lambda (server method params) + (apply fn server method (append params nil))))) (server (apply #'make-instance class From 7e7a9483a62be14bb62e7c4af1ccbfae81421002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 7 Dec 2018 22:51:40 +0000 Subject: [PATCH 360/771] Be lenient by default to unknown methods or notifications * eglot.el (eglot-strict-mode): Describe meaning of disallow-non-standard-keys. (eglot-handle-notification, eglot-handle-request): Check eglot-strict-mode. --- lisp/progmodes/eglot.el | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 05971e13cd2..f442b2fb244 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -260,7 +260,8 @@ Here's what an element of this alist might look like: )) "How strictly to check LSP interfaces at compile- and run-time. -Value is a list of symbols: +Value is a list of symbols (if the list is empty, no checks are +performed). If the symbol `disallow-non-standard-keys' is present, an error is raised if any extraneous fields are sent by the server. At @@ -276,9 +277,9 @@ If the symbol `enforce-optional-keys' is present, nothing special happens at run-time. At compile-time, a warning is raised if a destructuring spec doesn't use all optional fields. -If the list is empty, any non-standard fields sent by the server -and missing required fields are accepted (which may or may not -cause problems in Eglot's functioning later on).")) +If the symbol `disallow-unknown-methods' is present, Eglot warns +on unknown notifications and errors on unknown requests. +")) (defun eglot--plist-keys (plist) (cl-loop for (k _v) on plist by #'cddr collect k)) @@ -1316,13 +1317,15 @@ Uses THING, FACE, DEFS and PREPEND." (cl-defmethod eglot-handle-notification (_server method &key &allow-other-keys) "Handle unknown notification" - (unless (string-prefix-p "$" (format "%s" method)) + (unless (or (string-prefix-p "$" (format "%s" method)) + (not (memq 'disallow-unknown-methods eglot-strict-mode))) (eglot--warn "Server sent unknown notification method `%s'" method))) (cl-defmethod eglot-handle-request (_server method &key &allow-other-keys) "Handle unknown request" - (jsonrpc-error "Unknown request method `%s'" method)) + (when (memq 'disallow-unknown-methods eglot-strict-mode) + (jsonrpc-error "Unknown request method `%s'" method))) (cl-defmethod eglot-execute-command (server command arguments) From 89e8803f61839e5914344db846ae212be429f988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 10 Dec 2018 00:10:57 +0000 Subject: [PATCH 361/771] * eglot.el (version): bump to 1.3 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f442b2fb244..db4fbb4a7a5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.2 +;; Version: 1.3 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From d44340815666c633e0d20eb32959f45140664dc9 Mon Sep 17 00:00:00 2001 From: Fredrik Bergroth Date: Thu, 13 Dec 2018 12:55:28 +0100 Subject: [PATCH 362/771] Apply eglot--format-markup to signature documentation Copyright-paperwork-exempt: yes * eglot.el (eglot--sig-info): Call eglot--format-markup on signature documentation. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index db4fbb4a7a5..2b51c2155bc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1951,7 +1951,7 @@ is not active." (setq documentation (match-string 1 documentation)) (unless (string-prefix-p (string-trim documentation) label) (goto-char (point-max)) - (insert ": " documentation))) + (insert ": " (eglot--format-markup documentation)))) (when (and (eql i active-sig) active-param (< -1 active-param (length parameters))) (eglot--dbind ((ParameterInformation) label documentation) From cdee0e4674d837469b12b9378e24950bea82a555 Mon Sep 17 00:00:00 2001 From: Fredrik Bergroth Date: Thu, 13 Dec 2018 12:59:57 +0100 Subject: [PATCH 363/771] Adjust active param highlighting in first line of signature (1/3) JT@2018/12/16: Previously, the whole first line of the rendered documentation was searched for, potentially highlighting params in the wrong place. Copyright-paperwork-exempt: yes * eglot.el (eglot--sig-info): Search for active parameter within `params-start` and `params-end`. --- lisp/progmodes/eglot.el | 63 +++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2b51c2155bc..46cfb595fe5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1941,38 +1941,41 @@ is not active." (eglot--dbind ((SignatureInformation) label documentation parameters) sig (with-temp-buffer (save-excursion (insert label)) - (when (looking-at "\\([^(]+\\)(") - (add-face-text-property (match-beginning 1) (match-end 1) - 'font-lock-function-name-face)) + (let ((params-start (point-min)) + (params-end (point-max))) + (when (looking-at "\\([^(]+\\)(") + (setq params-start (match-end 0)) + (add-face-text-property (match-beginning 1) (match-end 1) + 'font-lock-function-name-face)) - (when (and (stringp documentation) (eql i active-sig) - (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" - documentation)) - (setq documentation (match-string 1 documentation)) - (unless (string-prefix-p (string-trim documentation) label) - (goto-char (point-max)) - (insert ": " (eglot--format-markup documentation)))) - (when (and (eql i active-sig) active-param - (< -1 active-param (length parameters))) - (eglot--dbind ((ParameterInformation) label documentation) - (aref parameters active-param) - (goto-char (point-min)) - (let ((case-fold-search nil)) - (cl-loop for nmatches from 0 - while (and (not (string-empty-p label)) - (search-forward label nil t)) - finally do - (when (= 1 nmatches) - (add-face-text-property - (- (point) (length label)) (point) - 'eldoc-highlight-function-argument)))) - (when documentation + (when (and (stringp documentation) (eql i active-sig) + (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" + documentation)) + (setq documentation (match-string 1 documentation)) + (unless (string-prefix-p (string-trim documentation) label) (goto-char (point-max)) - (insert "\n" - (propertize - label 'face 'eldoc-highlight-function-argument) - ": " (eglot--format-markup documentation))))) - (buffer-string))) + (insert ": " (eglot--format-markup documentation)))) + (when (and (eql i active-sig) active-param + (< -1 active-param (length parameters))) + (eglot--dbind ((ParameterInformation) label documentation) + (aref parameters active-param) + (goto-char params-start) + (let ((case-fold-search nil)) + (cl-loop for nmatches from 0 + while (and (not (string-empty-p label)) + (search-forward label params-end t)) + finally do + (when (= 1 nmatches) + (add-face-text-property + (- (point) (length label)) (point) + 'eldoc-highlight-function-argument)))) + (when documentation + (goto-char (point-max)) + (insert "\n" + (propertize + label 'face 'eldoc-highlight-function-argument) + ": " (eglot--format-markup documentation))))) + (buffer-string)))) when moresigs concat "\n")) (defun eglot-help-at-point () From d050540fefed7547a85be3c9eb2e71581a98724b Mon Sep 17 00:00:00 2001 From: Fredrik Bergroth Date: Thu, 13 Dec 2018 13:02:15 +0100 Subject: [PATCH 364/771] Adjust active param highlighting in first line of signature (2/3) Use regex with word boundaries when scanning for active param, to avoid matching substrings. Copyright-paperwork-exempt: yes * eglot.el (eglot--sig-info): Use `re-search-forward`. --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 46cfb595fe5..ae18493f11c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1960,10 +1960,10 @@ is not active." (eglot--dbind ((ParameterInformation) label documentation) (aref parameters active-param) (goto-char params-start) - (let ((case-fold-search nil)) + (let ((regex (concat "\\<" (regexp-quote label) "\\>")) + (case-fold-search nil)) (cl-loop for nmatches from 0 - while (and (not (string-empty-p label)) - (search-forward label params-end t)) + while (re-search-forward regex params-end t) finally do (when (= 1 nmatches) (add-face-text-property From f0a2747ab50b9b0b944655c43ae230ccfb7db1f8 Mon Sep 17 00:00:00 2001 From: Fredrik Bergroth Date: Thu, 13 Dec 2018 13:03:42 +0100 Subject: [PATCH 365/771] Adjust active param highlighting in first line of signature (3/3) Highlight only first active parameter match (even if there are many) Copyright-paperwork-exempt: yes * eglot.el (eglot--sig-info): Simplify. --- lisp/progmodes/eglot.el | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ae18493f11c..01f6960047f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1962,13 +1962,10 @@ is not active." (goto-char params-start) (let ((regex (concat "\\<" (regexp-quote label) "\\>")) (case-fold-search nil)) - (cl-loop for nmatches from 0 - while (re-search-forward regex params-end t) - finally do - (when (= 1 nmatches) - (add-face-text-property - (- (point) (length label)) (point) - 'eldoc-highlight-function-argument)))) + (when (re-search-forward regex params-end t) + (add-face-text-property + (- (point) (length label)) (point) + 'eldoc-highlight-function-argument))) (when documentation (goto-char (point-max)) (insert "\n" From cdb3de6bc6f9a1d4f642d1613019f947e7138c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 16 Dec 2018 13:48:57 +0000 Subject: [PATCH 366/771] Rewrite eglot--sig-info a bit for readability * eglot.el (eglot--sig-info): Rewrite a bit. --- lisp/progmodes/eglot.el | 61 ++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 01f6960047f..429c329c824 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1941,37 +1941,42 @@ is not active." (eglot--dbind ((SignatureInformation) label documentation parameters) sig (with-temp-buffer (save-excursion (insert label)) - (let ((params-start (point-min)) - (params-end (point-max))) - (when (looking-at "\\([^(]+\\)(") - (setq params-start (match-end 0)) + (let (params-start params-end) + ;; Ad-hoc attempt to parse label as () + (when (looking-at "\\([^(]+\\)(\\([^)]+\\))") + (setq params-start (match-beginning 2) params-end (match-end 2)) (add-face-text-property (match-beginning 1) (match-end 1) 'font-lock-function-name-face)) - - (when (and (stringp documentation) (eql i active-sig) - (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" - documentation)) - (setq documentation (match-string 1 documentation)) - (unless (string-prefix-p (string-trim documentation) label) - (goto-char (point-max)) - (insert ": " (eglot--format-markup documentation)))) - (when (and (eql i active-sig) active-param - (< -1 active-param (length parameters))) - (eglot--dbind ((ParameterInformation) label documentation) - (aref parameters active-param) - (goto-char params-start) - (let ((regex (concat "\\<" (regexp-quote label) "\\>")) - (case-fold-search nil)) - (when (re-search-forward regex params-end t) - (add-face-text-property - (- (point) (length label)) (point) - 'eldoc-highlight-function-argument))) - (when documentation + (when (eql i active-sig) + ;; Decide whether to add one-line-summary to signature line + (when (and (stringp documentation) + (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" + documentation)) + (setq documentation (match-string 1 documentation)) + (unless (string-prefix-p (string-trim documentation) label) (goto-char (point-max)) - (insert "\n" - (propertize - label 'face 'eldoc-highlight-function-argument) - ": " (eglot--format-markup documentation))))) + (insert ": " (eglot--format-markup documentation)))) + ;; Decide what to do with the active parameter... + (when (and (eql i active-sig) active-param + (< -1 active-param (length parameters))) + (eglot--dbind ((ParameterInformation) label documentation) + (aref parameters active-param) + ;; ...perhaps highlight it in the formals list + (when params-start + (goto-char params-start) + (let ((regex (concat "\\<" (regexp-quote label) "\\>")) + (case-fold-search nil)) + (when (re-search-forward regex params-end t) + (add-face-text-property + (match-beginning 0) (match-end 0) + 'eldoc-highlight-function-argument)))) + ;; ...and/or maybe add its doc on a line by its own. + (when documentation + (goto-char (point-max)) + (insert "\n" + (propertize + label 'face 'eldoc-highlight-function-argument) + ": " (eglot--format-markup documentation)))))) (buffer-string)))) when moresigs concat "\n")) From 47f5fdcae6b3f2177b55ec5898a8277b0d541a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 16 Dec 2018 13:51:14 +0000 Subject: [PATCH 367/771] Add edebug specs to destructuring macros * eglot.el (eglot--dbind, eglot--lambda, eglot--dcase): Add edebug specs. --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 429c329c824..e83562fd201 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -340,7 +340,7 @@ on unknown notifications and errors on unknown requests. "Destructure OBJECT of binding VARS in BODY. VARS is ([(INTERFACE)] SYMS...) Honour `eglot-strict-mode'." - (declare (indent 2)) + (declare (indent 2) (debug (sexp sexp &rest form))) (let ((interface-name (if (consp (car vars)) (car (pop vars)))) (object-once (make-symbol "object-once")) @@ -366,7 +366,7 @@ Honour `eglot-strict-mode'." (cl-defmacro eglot--lambda (cl-lambda-list &body body) "Function of args CL-LAMBDA-LIST for processing INTERFACE objects. Honour `eglot-strict-mode'." - (declare (indent 1)) + (declare (indent 1) (debug (sexp &rest form))) (let ((e (cl-gensym "jsonrpc-lambda-elem"))) `(lambda (,e) (eglot--dbind ,cl-lambda-list ,e ,@body)))) @@ -374,7 +374,7 @@ Honour `eglot-strict-mode'." "Like `pcase', but for the LSP object OBJ. CLAUSES is a list (DESTRUCTURE FORMS...) where DESTRUCTURE is treated as in `eglot-dbind'." - (declare (indent 1)) + (declare (indent 1) (debug (sexp &rest (sexp &rest form)))) (let ((obj-once (make-symbol "obj-once"))) `(let ((,obj-once ,obj)) (cond From 1d72360e03a219ffeb1799263a640f92c4e0c0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 16 Dec 2018 14:22:57 +0000 Subject: [PATCH 368/771] Don't make bogus responses to client/(un)registercapability * eglot.el (eglot--register-unregister): Response is void. --- lisp/progmodes/eglot.el | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e83562fd201..ee40329ca1b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1417,11 +1417,9 @@ COMMAND is a symbol naming the command." THINGS are either registrations or unregisterations." (cl-loop for thing in (cl-coerce things 'list) - collect (eglot--dbind ((Registration) id method registerOptions) thing - (apply (intern (format "eglot--%s-%s" how method)) - server :id id registerOptions)) - into results - finally return `(:ok ,@results))) + do (eglot--dbind ((Registration) id method registerOptions) thing + (apply (intern (format "eglot--%s-%s" how method)) + server :id id registerOptions)))) (cl-defmethod eglot-handle-request (server (_method (eql client/registerCapability)) &key registrations) From 24a1a7ffeed8d2e4b45a29a51b0378a87584e562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 16 Dec 2018 14:33:14 +0000 Subject: [PATCH 369/771] Be more careful when making xref summaries * eglot.el (eglot--xref-make): Only highlight to end-of-line at most. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/187 --- lisp/progmodes/eglot.el | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ee40329ca1b..60dad452d68 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1667,8 +1667,10 @@ Try to visit the target file for a richer summary line." (eglot--widening (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) (bol (progn (goto-char beg) (point-at-bol))) - (substring (buffer-substring bol (point-at-eol)))) - (add-face-text-property (- beg bol) (- end bol) 'highlight + (substring (buffer-substring bol (point-at-eol))) + (hi-beg (- beg bol)) + (hi-end (- (min (point-at-eol) end) bol))) + (add-face-text-property hi-beg hi-end 'highlight t substring) (list substring (1+ (current-line)) (eglot-current-column)))))) (`(,summary ,line ,column) From d9e4306e2d96e17cd99d610f865c79fdf034e98c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 16 Dec 2018 19:03:06 +0000 Subject: [PATCH 370/771] Take over flymake and eldoc completely while managing buffers Take a pragmatic approach and override all other Flymake and Eglot backends while Eglot is enabled. Restore previous values after eglot-shutdown. Certainly cases might arise where using more than one datasource besides LSP while Eglot is managing the buffer is useful. But currently contrary, it confuses users enabling Eglot in buffers that already have flymake/eldoc backends configured. The reasons are slightly different for Eldoc and Flymake: - For Eldoc the :before-until strategy only makes sense for synchronous backends, which Eglot isn't. This conflicts with python.el default python-eldoc-function, which is also asynchronous. - For Flymake, the default backends in Emacs (python-mode, c-mode, and a few others) are mainly repetitions of what LSP does. The global value is still run though (in case you want to put, say, a spell-checking backend there). * eglot.el (eglot--saved-bindings, eglot--setq-saving): New helpers. (eglot--managed-mode): Use them. --- lisp/progmodes/eglot.el | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 60dad452d68..7f3eea4748a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1118,6 +1118,14 @@ and just return it. PROMPT shouldn't end with a question mark." (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") +(defvar-local eglot--saved-bindings nil + "Bindings saved by `eglot--setq-saving'.") + +(defmacro eglot--setq-saving (symbol binding) + `(progn (push (cons ',symbol (symbol-value ',symbol)) + eglot--saved-bindings) + (setq-local ,symbol ,binding))) + (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map @@ -1125,7 +1133,6 @@ and just return it. PROMPT shouldn't end with a question mark." (eglot--managed-mode (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) - (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t) (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'kill-buffer-hook 'eglot--managed-mode-onoff nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) @@ -1136,8 +1143,8 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff 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) - (add-function :before-until (local 'eldoc-documentation-function) - #'eglot-eldoc-function) + (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) + (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) @@ -1154,9 +1161,8 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) - (remove-function (local 'eldoc-documentation-function) - #'eglot-eldoc-function) - (remove-function (local 'imenu-create-index-function) #'eglot-imenu) + (cl-loop for (var . saved-binding) in eglot--saved-bindings + do (set (make-local-variable var) saved-binding)) (setq eglot--current-flymake-report-fn nil)))) (defvar-local eglot--cached-current-server nil From 00fb3a184abd39dbdb9316faa83b4045fdf075a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 19 Dec 2018 21:57:00 +0000 Subject: [PATCH 371/771] * eglot.el (package-requires): require jsonrpc 1.0.7. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7f3eea4748a..108e7f0d920 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.6") (flymake "1.0.2")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.7") (flymake "1.0.2")) ;; 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 From b699fc7a195d7e1ae983cca5839010033915a3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 22 Dec 2018 15:17:41 +0000 Subject: [PATCH 372/771] Remove workaround for company bug that has been fixed See https://github.com/company-mode/company-mode/pull/845. * eglot.el (eglot-completion-at-point): Remove workaround for company-mode bug. --- lisp/progmodes/eglot.el | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 108e7f0d920..53a52bfc421 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1822,22 +1822,19 @@ is not active." :deferred :textDocument/completion :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (setq - strings - (mapcar - (jsonrpc-lambda (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) - (put-text-property 0 1 'eglot--completion-bounds bounds completion) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items))))) + (mapcar + (jsonrpc-lambda (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) + items)))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) @@ -1898,12 +1895,7 @@ is not active." additionalTextEdits) (get-text-property 0 'eglot--lsp-completion comp) (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn))) - ;; FIXME: it would have been much easier to fetch - ;; these from the lexical environment, but we can't - ;; in company because of - ;; https://github.com/company-mode/company-mode/pull/845 - (bounds (get-text-property 0 'eglot--completion-bounds comp))) + (eglot--snippet-expansion-fn)))) (cond (textEdit ;; Undo the just the completed bit. If before ;; completion the buffer was "foo.b" and now is From d255e51c78fe99a0e2736305f0baa2155cb9ca70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 22 Dec 2018 15:18:55 +0000 Subject: [PATCH 373/771] Use gfm-view-mode * eglot.el (eglot--format-markup): Use gfm-view-mode instead of gfm-mode. GitHub-reference: per https://github.com/joaotavora/eglot/issues/188 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 53a52bfc421..43bc023509e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1026,7 +1026,7 @@ Doubles as an indicator of snippet support." "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) (if (stringp markup) (list (string-trim markup) - (intern "gfm-mode")) + (intern "gfm-view-mode")) (list (plist-get markup :value) major-mode)))) (with-temp-buffer From f5e3279958eb78c90da8df3a159b931b7a6b3134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 22 Dec 2018 15:27:27 +0000 Subject: [PATCH 374/771] Fix previous commit where workaround had been removed Do remove the workaround, but not more than that. * eglot.el (eglot-completion-at-point): set local var strings. --- lisp/progmodes/eglot.el | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 43bc023509e..5861b05fcdf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1822,19 +1822,21 @@ is not active." :deferred :textDocument/completion :cancel-on-input t)) (items (if (vectorp resp) resp (plist-get resp :items)))) - (mapcar - (jsonrpc-lambda (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items)))) + (setq + strings + (mapcar + (jsonrpc-lambda (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) + items))))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) From 9ffa6a91cd1b7c84134997e22295664eba1925d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 22 Dec 2018 15:23:41 +0000 Subject: [PATCH 375/771] Actually make completion sorting work * eglot.el (eglot-completion-at-point): Complicate severely. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/190 --- lisp/progmodes/eglot.el | 66 ++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5861b05fcdf..d60fc0776a8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1806,37 +1806,49 @@ is not active." (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." - (let ((bounds (bounds-of-thing-at-point 'symbol)) + (let* ((bounds (bounds-of-thing-at-point 'symbol)) (server (eglot--current-server-or-lose)) (completion-capability (eglot--server-capable :completionProvider)) + (sort-completions (lambda (completions) + (sort completions + (lambda (a b) + (string-lessp + (or (get-text-property 0 :sortText a) "") + (or (get-text-property 0 :sortText b) "")))))) + (metadata `(metadata . ((display-sort-function . ,sort-completions)))) strings) (when completion-capability (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (completion-table-dynamic - (lambda (_ignored) - (let* ((resp (jsonrpc-request server - :textDocument/completion - (eglot--CompletionParams) - :deferred :textDocument/completion - :cancel-on-input t)) - (items (if (vectorp resp) resp (plist-get resp :items)))) - (setq - strings - (mapcar - (jsonrpc-lambda (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items))))) + (lambda (string pred action) + (if (eq action 'metadata) metadata + (funcall + (completion-table-dynamic + (lambda (_ignored) + (let* ((resp (jsonrpc-request server + :textDocument/completion + (eglot--CompletionParams) + :deferred :textDocument/completion + :cancel-on-input t)) + (items (if (vectorp resp) resp (plist-get resp :items)))) + (setq + strings + (mapcar + (jsonrpc-lambda + (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (add-text-properties 0 1 all completion) + (put-text-property 0 1 'eglot--lsp-completion all completion) + completion)) + items))))) + string pred action))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) @@ -1854,12 +1866,6 @@ is not active." (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn) " (snippet)")))))) - :display-sort-function - (lambda (items) - (sort items (lambda (a b) - (string-lessp - (or (get-text-property 0 :sortText a) "") - (or (get-text-property 0 :sortText b) ""))))) :company-doc-buffer (lambda (obj) (let* ((documentation From 5df556bb94d4dcd7aedc1dd8d282b553f67f15e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 23 Dec 2018 14:01:38 +0000 Subject: [PATCH 376/771] Slightly simplify eglot-completion-at-point * eglot.el (eglot-completion-at-point): Don't propertize completion string with all LSP properties. --- lisp/progmodes/eglot.el | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d60fc0776a8..a335f1675eb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1844,7 +1844,6 @@ is not active." (string-trim-left label)) (t (or insertText (string-trim-left label)))))) - (add-text-properties 0 1 all completion) (put-text-property 0 1 'eglot--lsp-completion all completion) completion)) items))))) @@ -1869,15 +1868,15 @@ is not active." :company-doc-buffer (lambda (obj) (let* ((documentation - (or (get-text-property 0 :documentation obj) - (and (eglot--server-capable :completionProvider - :resolveProvider) - (plist-get - (jsonrpc-request server :completionItem/resolve - (get-text-property - 0 'eglot--lsp-completion obj) - :cancel-on-input t) - :documentation)))) + (let ((lsp-comp + (get-text-property 0 'eglot--lsp-completion obj))) + (or (plist-get lsp-comp :documentation) + (and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get + (jsonrpc-request server :completionItem/resolve + lsp-comp :cancel-on-input t) + :documentation))))) (formatted (and documentation (eglot--format-markup documentation)))) (when formatted From 0c432de4cf64dca3869a4c7578e9d21fe3bef491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 27 Dec 2018 18:31:42 +0000 Subject: [PATCH 377/771] Remove a hard dependency on flymake-mode * eglot.el (eglot-handle-notification): Don't specifically check for flymake-mode before reporting diagnostics. GitHub-reference: close https://github.com/joaotavora/eglot/issues/195 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a335f1675eb..2411c6a89c9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1405,7 +1405,7 @@ COMMAND is a symbol naming the command." (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) into diags - finally (cond ((and flymake-mode eglot--current-flymake-report-fn) + finally (cond (eglot--current-flymake-report-fn (funcall eglot--current-flymake-report-fn diags ;; If the buffer hasn't changed since last ;; call to the report function, flymake won't From 355c9c4a656719a94e0a25a04eb913470d46b7dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 1 Jan 2019 14:56:46 +0000 Subject: [PATCH 378/771] Allow read-only modes for markup rendering gfm-mode is read-only, so it must be set after the string has been inserted in the temporary buffer. * eglot.el (eglot--format-markup): Insert string before setting mode. GitHub-reference: close https://github.com/joaotavora/eglot/issues/197 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2411c6a89c9..4416e52c705 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1030,8 +1030,8 @@ Doubles as an indicator of snippet support." (list (plist-get markup :value) major-mode)))) (with-temp-buffer - (ignore-errors (funcall mode)) - (insert string) (font-lock-ensure) (buffer-string)))) + (insert string) + (ignore-errors (funcall mode)) (font-lock-ensure) (buffer-string)))) (defcustom eglot-ignored-server-capabilites (list) "LSP server capabilities that Eglot could use, but won't. From f3914c266fcf81bb735e733222d6a4aa70948548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 Jan 2019 17:12:36 +0000 Subject: [PATCH 379/771] Run connection hooks with proper dir-locals eglot-connect-hook and eglot-server-initialized-hook must run in a buffer with properly setup directory-local variables for the project. This is crucial for things like eglot-signal-didChangeConfiguration, which needs a properly setup value of eglot-workspace-configuration to succeed. I could have chosen any of the buffers where Eglot is activating itself, but the approach using hack-dir-local-variables-non-file-buffer seems more correct, despite the name. * eglot.el (eglot--connect): Run connection hooks with proper dir-locals. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/196 --- lisp/progmodes/eglot.el | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4416e52c705..39f484220aa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -840,8 +840,11 @@ This docstring appeases checkdoc, that's all." (lambda () (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) - (run-hook-with-args 'eglot-connect-hook server) - (run-hook-with-args 'eglot-server-initialized-hook server) + (let ((default-directory (car (project-roots project))) + (major-mode managed-major-mode)) + (hack-dir-local-variables-non-file-buffer) + (run-hook-with-args 'eglot-connect-hook server) + (run-hook-with-args 'eglot-server-initialized-hook server)) (eglot--message "Connected! Server `%s' now managing `%s' buffers \ in project `%s'." From 36b2fa8e7f799679c1ba6995e097ea3bd0fbc72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 2 Jan 2019 20:34:09 +0000 Subject: [PATCH 380/771] * eglot.el (eglot-workspace-configuration): safe when listp. --- lisp/progmodes/eglot.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 39f484220aa..6de3adf9763 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1553,6 +1553,8 @@ Records START, END and PRE-CHANGE-LENGTH locally." Setting should be a keyword, value can be any value that can be converted to JSON.") +(put 'eglot-workspace-configuration 'safe-local-variable 'listp) + (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. When called interactively, use the currently active server" From 6ee4328ca1495afb90fa8b3377781e1b3667ec5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 Jan 2019 13:03:01 +0000 Subject: [PATCH 381/771] Appease checkdoc * eglot.el (eglot--post-self-insert-hook) (eglot--pre-command-hook, eglot--before-change) (eglot--eclipse-jdt-contact): Fix docstrings. --- lisp/progmodes/eglot.el | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6de3adf9763..53e60fd6ef0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1481,11 +1481,11 @@ THINGS are either registrations or unregisterations." "If non-nil, value of the last inserted character in buffer.") (defun eglot--post-self-insert-hook () - "Set `eglot--last-inserted-char.'" + "Set `eglot--last-inserted-char'." (setq eglot--last-inserted-char last-input-event)) (defun eglot--pre-command-hook () - "Reset `eglot--last-inserted-char.'" + "Reset `eglot--last-inserted-char'." (setq eglot--last-inserted-char nil)) (defun eglot--CompletionParams () @@ -1510,7 +1510,7 @@ THINGS are either registrations or unregisterations." (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") (defun eglot--before-change (start end) - "Hook onto `before-change-functions'." + "Hook onto `before-change-functions' with START and END." ;; Records START and END, crucially convert them into LSP ;; (line/char) positions before that information is lost (because ;; the after-change thingy doesn't know if newlines were @@ -2352,7 +2352,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (ignore (eglot--warn "JAVA_HOME env var not set"))))) (defun eglot--eclipse-jdt-contact (interactive) - "Return a contact for connecting to eclipse.jdt.ls server, as a cons cell." + "Return a contact for connecting to eclipse.jdt.ls server, as a cons cell. +If INTERACTIVE, prompt user for details." (cl-labels ((is-the-jar (path) From a47618f19f6dec030b08e04717e10a573a1ce14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 Jan 2019 13:32:13 +0000 Subject: [PATCH 382/771] Handle (un)registercapability requests via generic functions * eglot.el (Version): Bump to 1.4 (eglot-register-capability, eglot-unregister-capability): New generic functions. (eglot--register-unregister): Call eglot-register-capability, eglot-unregister-capability. (eglot-register-capability s (eql workspace/didChangeWatchedFiles)): Rename from eglot--register-workspace/didChangeWatchedFiles. (eglot-unregister-capability s (eql workspace/didChangeWatchedFiles)): Rename from eglot--unregister-workspace/didChangeWatchedFiles. --- lisp/progmodes/eglot.el | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 53e60fd6ef0..031a657fe90 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.3 +;; Version: 1.4 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot @@ -440,6 +440,20 @@ treated as in `eglot-dbind'." "JSON object to send under `initializationOptions'" (:method (_s) nil)) ; blank default +(cl-defgeneric eglot-register-capability (server method id &rest params) + "Ask SERVER to register capability METHOD marked with ID." + (:method + (_s method _id &rest _params) + (eglot--warn "Server tried to register unsupported capability `%s'" + method))) + +(cl-defgeneric eglot-unregister-capability (server method id &rest params) + "Ask SERVER to register capability METHOD marked with ID." + (:method + (_s method _id &rest _params) + (eglot--warn "Server tried to unregister unsupported capability `%s'" + method))) + (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." (:method (_s) @@ -1423,12 +1437,14 @@ COMMAND is a symbol naming the command." (cl-defun eglot--register-unregister (server things how) "Helper for `registerCapability'. -THINGS are either registrations or unregisterations." +THINGS are either registrations or unregisterations (sic)." (cl-loop for thing in (cl-coerce things 'list) do (eglot--dbind ((Registration) id method registerOptions) thing - (apply (intern (format "eglot--%s-%s" how method)) - server :id id registerOptions)))) + (apply (cl-ecase how + (register 'eglot-register-capability) + (unregister 'eglot-unregister-capability)) + server (intern method) id registerOptions)))) (cl-defmethod eglot-handle-request (server (_method (eql client/registerCapability)) &key registrations) @@ -2243,9 +2259,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." for result = (replace-regexp-in-string pattern rep target) finally return result)) -(cl-defun eglot--register-workspace/didChangeWatchedFiles (server &key id watchers) +(cl-defmethod eglot-register-capability + (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" - (eglot--unregister-workspace/didChangeWatchedFiles server :id id) + (eglot-unregister-capability server method id) (let* (success (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) globPattern) @@ -2280,9 +2297,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." `(:message ,(format "OK, watching %s watchers" (length watchers))))) (unless success - (eglot--unregister-workspace/didChangeWatchedFiles server :id id)))))) + (eglot-unregister-capability server method id)))))) -(cl-defun eglot--unregister-workspace/didChangeWatchedFiles (server &key id) +(cl-defmethod eglot-unregister-capability + (server (_method (eql workspace/didChangeWatchedFiles)) id) "Handle dynamic unregistration of workspace/didChangeWatchedFiles" (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches server))) (remhash id (eglot--file-watches server)) From f399be6d122d9f23da8a13df8050f213c0617369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 Jan 2019 14:35:01 +0000 Subject: [PATCH 383/771] Prevent eldoc flicker when moving around * eglot.el (eglot-eldoc-function): Return eldoc-last-message immediately. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/198 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 031a657fe90..98032b4a3e5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2070,7 +2070,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ov))) highlights)))) :deferred :textDocument/documentHighlight)))) - nil) + eldoc-last-message) (defun eglot-imenu (oldfun) "EGLOT's `imenu-create-index-function' overriding OLDFUN." From aedb0d33de82595822a6fe56f40fdf608adc37c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 Jan 2019 22:47:43 +0000 Subject: [PATCH 384/771] Show large docs in help buffer instead of echo are by default * eglot.el (eglot--managed-mode): Add and remove from eglot--eldoc-message (eglot--eldoc-hint, eglot--help-buffer): New helpers. (eglot-eldoc-extra-buffer) (eglot-auto-display-eldoc-extra-buffer): New defcustoms. (eglot--eldoc-message): New helper. (eglot-eldoc-function): Set eglot--eldoc-hint. (eglot-help-at-point): Use new helpers. (eglot-eldoc-extra-buffer-if-too-large): New predicate. GitHub-reference: per https://github.com/joaotavora/eglot/issues/198 --- lisp/progmodes/eglot.el | 79 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98032b4a3e5..fb002b85a8e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1162,6 +1162,7 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) + (add-function :before-until (local 'eldoc-message-function) #'eglot--eldoc-message) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) @@ -1178,6 +1179,7 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) + (remove-function (local 'eldoc-message-function) #'eglot--eldoc-message) (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) (setq eglot--current-flymake-report-fn nil)))) @@ -2006,6 +2008,15 @@ is not active." (buffer-string)))) when moresigs concat "\n")) +(defvar eglot--eldoc-hint nil) + +(defvar eglot--help-buffer nil) + +(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-help-at-point () "Request \"hover\" information for the thing at point." (interactive) @@ -2013,9 +2024,58 @@ is not active." (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (when (seq-empty-p contents) (eglot--error "No hover info here")) - (let ((blurb (eglot--hover-info contents range))) - (with-help-window "*eglot help*" - (with-current-buffer standard-output (insert blurb)))))) + (let ((blurb (eglot--hover-info contents range)) + (sym (thing-at-point 'symbol))) + (with-current-buffer (eglot--help-buffer) + (with-help-window (current-buffer) + (rename-buffer (format "*eglot-help for %s*" sym)) + (with-current-buffer standard-output (insert blurb))))))) + +(defun eglot-eldoc-extra-buffer-if-too-large (string) + "Return non-nil if STRING won't fit in echo area. +Respects `max-mini-window-height' (which see)." + (let ((max-height + (cond ((floatp max-mini-window-height) (* (frame-height) + max-mini-window-height)) + ((integerp max-mini-window-height) max-mini-window-height) + (t 1)))) + (> (cl-count ?\n string) max-height))) + +(defcustom eglot-eldoc-extra-buffer + #'eglot-eldoc-extra-buffer-if-too-large + "If non-nil, put eldoc docstrings in separate `*eglot-help*' buffer. +If nil, use whatever `eldoc-message-function' decides (usually +the echo area). If t, use `*eglot-help; unconditionally. If a +function, it is called with the docstring to display and should a +boolean." + :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-eldoc-extra-buffer nil + "If non-nil, automatically display `*eglot-help*' buffer. +Buffer is displayed with `display-buffer', which obeys +`display-buffer-alist' & friends." + :type 'boolean) + +(defun eglot--eldoc-message (format &rest args) + (let ((string (apply #'format format args))) ;; FIXME: overworking? + (when (or (eq t eglot-eldoc-extra-buffer) + (funcall eglot-eldoc-extra-buffer string)) + (with-current-buffer (eglot--help-buffer) + (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert string) + (goto-char (point-min)) + (setq eldoc-last-message nil) + (if eglot-auto-display-eldoc-extra-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message "Help for %s in in %s buffer" eglot--eldoc-hint + (buffer-name eglot--help-buffer)))) + (help-mode) + t))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function. @@ -2023,7 +2083,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* ((buffer (current-buffer)) (server (eglot--current-server-or-lose)) (position-params (eglot--TextDocumentPositionParams)) - sig-showing) + 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)) @@ -2037,9 +2098,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-buffer-window (when (cl-plusp (length signatures)) (setq sig-showing t) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter))))) + (let ((eglot--eldoc-hint thing-at-point)) + (eldoc-message (eglot--sig-info signatures + activeSignature + activeParameter)))))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (jsonrpc-async-request @@ -2050,7 +2112,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-let (info (and contents (eglot--hover-info contents range))) - (eldoc-message info))))) + (let ((eglot--eldoc-hint thing-at-point)) + (eldoc-message info)))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (jsonrpc-async-request From 84234b25ba59dd593e0f77bb4a54c03281519567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 6 Jan 2019 12:55:21 +0000 Subject: [PATCH 385/771] Fix test failure introduced by previous commit Remove the hack of unsetting eldoc-last-message in eglot--eldoc-message. This allows any subsequent eglot-eldoc-function calls (prompted by simple cursor movement) to return it immediately, thus refreshing the help buffer with the same contents. For this to work, we also have to set eglot--eldoc-hint globally in eglot-eldoc-function. An alternative to making the test pass would be to keep the hack of unsetting eldoc-last-message only in the case that we actually get to display the help buffer. This would actually be more efficient, but potentially more hacky. The bottom line here is that eldoc doesn't have a good API to deal with asynchronous docstring fetching. See this thread: https://lists.gnu.org/archive/html/emacs-devel/2018-05/msg00151.html * eglot.el (eglot--eldoc-message): Don't unset eldoc-last-message. (eglot-eldoc-function): Set eglot--eldoc-hint for synchronous operation too. --- lisp/progmodes/eglot.el | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fb002b85a8e..a7740e56904 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2068,12 +2068,12 @@ Buffer is displayed with `display-buffer', which obeys (erase-buffer) (insert string) (goto-char (point-min)) - (setq eldoc-last-message nil) - (if eglot-auto-display-eldoc-extra-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - (eglot--message "Help for %s in in %s buffer" eglot--eldoc-hint - (buffer-name eglot--help-buffer)))) + (cond (eglot-auto-display-eldoc-extra-buffer + (display-buffer (current-buffer))) + (t + (unless (get-buffer-window (current-buffer)) + (eglot--message "Help for %s is in %s buffer" eglot--eldoc-hint + (buffer-name eglot--help-buffer))))) (help-mode) t))))) @@ -2085,6 +2085,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (position-params (eglot--TextDocumentPositionParams)) sig-showing (thing-at-point (thing-at-point 'symbol))) + (setq eglot--eldoc-hint thing-at-point) (cl-macrolet ((when-buffer-window (&body body) ; notice the exception when testing with `ert' `(when (or (get-buffer-window buffer) (ert-running-test)) From 791a117c5fab6f55ca5fbc03be0225444c4cd9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 6 Jan 2019 22:14:27 +0000 Subject: [PATCH 386/771] Rename new defcustoms with friendlier names * eglot.el (eglot-doc-too-large-for-echo-area): Rename from eglot-eldoc-extra-buffer-if-too-large. (eglot-put-doc-in-help-buffer): Rename from eglot-eldoc-extra-buffer. (eglot-auto-display-help-buffer): Rename from eglot-auto-display-eldoc-extra-buffer. (eglot--eldoc-message): Use new variable names. --- lisp/progmodes/eglot.el | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a7740e56904..9631c192e6c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2031,7 +2031,7 @@ is not active." (rename-buffer (format "*eglot-help for %s*" sym)) (with-current-buffer standard-output (insert blurb))))))) -(defun eglot-eldoc-extra-buffer-if-too-large (string) +(defun eglot-doc-too-large-for-echo-area (string) "Return non-nil if STRING won't fit in echo area. Respects `max-mini-window-height' (which see)." (let ((max-height @@ -2041,18 +2041,18 @@ Respects `max-mini-window-height' (which see)." (t 1)))) (> (cl-count ?\n string) max-height))) -(defcustom eglot-eldoc-extra-buffer - #'eglot-eldoc-extra-buffer-if-too-large - "If non-nil, put eldoc docstrings in separate `*eglot-help*' buffer. +(defcustom eglot-put-doc-in-help-buffer + #'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 (usually the echo area). If t, use `*eglot-help; unconditionally. If a function, it is called with the docstring to display and should a -boolean." +boolean producing one of the two previous 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-eldoc-extra-buffer nil +(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." @@ -2060,20 +2060,19 @@ Buffer is displayed with `display-buffer', which obeys (defun eglot--eldoc-message (format &rest args) (let ((string (apply #'format format args))) ;; FIXME: overworking? - (when (or (eq t eglot-eldoc-extra-buffer) - (funcall eglot-eldoc-extra-buffer string)) + (when (or (eq t eglot-put-doc-in-help-buffer) + (funcall eglot-put-doc-in-help-buffer string)) (with-current-buffer (eglot--help-buffer) (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) (let ((inhibit-read-only t)) (erase-buffer) (insert string) (goto-char (point-min)) - (cond (eglot-auto-display-eldoc-extra-buffer - (display-buffer (current-buffer))) - (t - (unless (get-buffer-window (current-buffer)) - (eglot--message "Help for %s is in %s buffer" eglot--eldoc-hint - (buffer-name eglot--help-buffer))))) + (if eglot-auto-display-help-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message "Help for %s is in %s buffer" eglot--eldoc-hint + (buffer-name eglot--help-buffer)))) (help-mode) t))))) From 58d4aff894c650ef4b3f601292314af53b961ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 7 Jan 2019 08:44:37 +0000 Subject: [PATCH 387/771] Display truncated docstring if too large for echo area * eglot.el (eglot--eldoc-message): Display first line doc. --- lisp/progmodes/eglot.el | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9631c192e6c..2ab4bd864a9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2071,8 +2071,12 @@ Buffer is displayed with `display-buffer', which obeys (if eglot-auto-display-help-buffer (display-buffer (current-buffer)) (unless (get-buffer-window (current-buffer)) - (eglot--message "Help for %s is in %s buffer" eglot--eldoc-hint - (buffer-name eglot--help-buffer)))) + (eglot--message + "%s\n(...truncated. Full help is in `%s')" + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...") + (buffer-name eglot--help-buffer)))) (help-mode) t))))) From 9cedae50a27f0d0d6adee82976654d63c9f67602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 Jan 2019 21:09:35 +0000 Subject: [PATCH 388/771] Handle label offsets in parameterinformation At least ccls uses this. * eglot.el (eglot-client-capabilities): Declare support for :labelOffsetSupport. (eglot--sig-info): Handle label offsets in ParameterInformation GitHub-reference: fix https://github.com/joaotavora/eglot/issues/201 --- lisp/progmodes/eglot.el | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ab4bd864a9..befa6cdf454 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -477,7 +477,10 @@ treated as in `eglot-dbind'." :json-false)) :contextSupport t) :hover `(:dynamicRegistration :json-false) - :signatureHelp `(:dynamicRegistration :json-false) + :signatureHelp (list :dynamicRegistration :json-false + :signatureInformation + `(:parameterInformation + (:labelOffsetSupport t))) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) :documentSymbol (list @@ -1992,12 +1995,18 @@ is not active." ;; ...perhaps highlight it in the formals list (when params-start (goto-char params-start) - (let ((regex (concat "\\<" (regexp-quote label) "\\>")) - (case-fold-search nil)) - (when (re-search-forward regex params-end t) - (add-face-text-property - (match-beginning 0) (match-end 0) - 'eldoc-highlight-function-argument)))) + (pcase-let + ((`(,beg ,end) + (if (stringp label) + (let ((case-fold-search nil)) + (and (re-search-forward + (concat "\\<" (regexp-quote label) "\\>") + params-end t) + (list (match-beginning 0) (match-end 0)))) + (mapcar #'1+ (append label nil))))) + (add-face-text-property + beg end + 'eldoc-highlight-function-argument))) ;; ...and/or maybe add its doc on a line by its own. (when documentation (goto-char (point-max)) From 1da5b8e1a31f3032baf26573f5ccb09a26a67375 Mon Sep 17 00:00:00 2001 From: Brady Trainor Date: Tue, 25 Dec 2018 16:21:53 -0800 Subject: [PATCH 389/771] Add built-in support for dart's dart_language_server Closes https://github.com/joaotavora/eglot/issues/194. Copyright-paperwork-exempt: yes * README.md (Connecting to a server): Add dart_language_server. * eglot.el (eglot-server-programs): Add dart_language_server. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index befa6cdf454..18067f61b56 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -100,7 +100,8 @@ language-server/bin/php-language-server.php")) "-gocodecompletion")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) - (java-mode . eglot--eclipse-jdt-contact)) + (java-mode . eglot--eclipse-jdt-contact) + (dart-mode . ("dart_language_server"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From d0c8c6011e774c91a35d14086506b34fb070682e Mon Sep 17 00:00:00 2001 From: Sergey Kostyaev Date: Fri, 11 Jan 2019 02:45:30 +0700 Subject: [PATCH 390/771] Fix bug introduced by commit fixing this issue * eglot.el (eglot--sig-info): Protect against invalid label. GitHub-reference: per https://github.com/joaotavora/eglot/issues/121 --- lisp/progmodes/eglot.el | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 18067f61b56..d251bcb7c09 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2005,9 +2005,10 @@ is not active." params-end t) (list (match-beginning 0) (match-end 0)))) (mapcar #'1+ (append label nil))))) - (add-face-text-property - beg end - 'eldoc-highlight-function-argument))) + (if (and beg end) + (add-face-text-property + beg end + 'eldoc-highlight-function-argument)))) ;; ...and/or maybe add its doc on a line by its own. (when documentation (goto-char (point-max)) From 1eb7535511add8a828290e0353ac6c9719b7af7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 Jan 2019 14:15:20 +0000 Subject: [PATCH 391/771] Protect against null messages from eldoc * eglot.el (eglot--eldoc-message): Protect against nil FORMAT. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/209 --- lisp/progmodes/eglot.el | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d251bcb7c09..799ab10fa29 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2070,26 +2070,27 @@ Buffer is displayed with `display-buffer', which obeys :type 'boolean) (defun eglot--eldoc-message (format &rest args) - (let ((string (apply #'format format args))) ;; FIXME: overworking? - (when (or (eq t eglot-put-doc-in-help-buffer) - (funcall eglot-put-doc-in-help-buffer string)) - (with-current-buffer (eglot--help-buffer) - (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert string) - (goto-char (point-min)) - (if eglot-auto-display-help-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - (eglot--message - "%s\n(...truncated. Full help is in `%s')" - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...") - (buffer-name eglot--help-buffer)))) - (help-mode) - t))))) + (when format + (let ((string (apply #'format format args))) ;; FIXME: overworking? + (when (or (eq t eglot-put-doc-in-help-buffer) + (funcall eglot-put-doc-in-help-buffer string)) + (with-current-buffer (eglot--help-buffer) + (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert string) + (goto-char (point-min)) + (if eglot-auto-display-help-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message + "%s\n(...truncated. Full help is in `%s')" + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...") + (buffer-name eglot--help-buffer)))) + (help-mode) + t)))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function. From 5292c4b6f01c2ac680eb5a032100486ce6047412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 Jan 2019 15:31:26 +0000 Subject: [PATCH 392/771] Consider mode derivation when guessing servers * eglot.el (eglot-server-programs): Remove js2-mode and rjsx-mode. (eglot--guess-contact): Use provided-mode-derived-p GitHub-reference: per https://github.com/joaotavora/eglot/issues/177 --- lisp/progmodes/eglot.el | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 799ab10fa29..2d1c3676f22 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -81,8 +81,6 @@ (defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) (python-mode . ("pyls")) ((js-mode - js2-mode - rjsx-mode typescript-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) @@ -635,8 +633,9 @@ be guessed." (project (or (project-current) `(transient . ,default-directory))) (guess (cdr (assoc managed-mode eglot-server-programs (lambda (m1 m2) - (or (eq m1 m2) - (and (listp m1) (memq m2 m1))))))) + (cl-find + m2 (if (listp m1) m1 (list m1)) + :test #'provided-mode-derived-p))))) (guess (if (functionp guess) (funcall guess interactive) guess)) From 36f294c2d5fb975f888539f9aef88100bf2db0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 5 Feb 2019 21:49:38 +0000 Subject: [PATCH 393/771] Don't teardown company if started via trigger chars Reported by zhanghj in https://github.com/company-mode/company-mode/issues/866 * eglot.el (eglot-completion-at-point): More carefully calculate :company-prefix-length --- lisp/progmodes/eglot.el | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2d1c3676f22..a4b7f8f8e63 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1911,9 +1911,12 @@ is not active." (insert formatted) (current-buffer))))) :company-prefix-length - (cl-some #'looking-back - (mapcar #'regexp-quote - (plist-get completion-capability :triggerCharacters))) + (save-excursion + (when (car bounds) (goto-char (car bounds))) + (looking-back + (regexp-opt + (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) + (line-beginning-position))) :exit-function (lambda (comp _status) (let ((comp (if (get-text-property 0 'eglot--lsp-completion comp) From 232289d25c0851e85b1798d976ed8c368b83cea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 11 Feb 2019 21:21:49 +0000 Subject: [PATCH 394/771] Use a less buggy flymake * eglot.el (Package-Requires) Require flymake 1.0.5 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/223 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a4b7f8f8e63..79e414646b3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.7") (flymake "1.0.2")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.7") (flymake "1.0.5")) ;; 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 From e123f41b9b2bc1b0c3ae9728362217bfc5a4770c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 11 Feb 2019 21:33:49 +0000 Subject: [PATCH 395/771] Don't sort xref's by default But use a eglot-xref-lessp-function in case someone wants to tweak this. * eglot.el (eglot-xref-lessp-function): New variable. (eglot--handling-xrefs): Use it. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/220 --- lisp/progmodes/eglot.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 79e414646b3..4fdd5c4e5be 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1678,13 +1678,13 @@ DUMMY is ignored." (defvar eglot--temp-location-buffers (make-hash-table :test #'equal) "Helper variable for `eglot--handling-xrefs'.") +(defvar eglot-xref-lessp-function #'ignore + "Compare two `xref-item' objects for sorting.") + (defmacro eglot--handling-xrefs (&rest body) "Properly sort and handle xrefs produced and returned by BODY." `(unwind-protect - (sort (progn ,@body) - (lambda (a b) - (< (xref-location-line (xref-item-location a)) - (xref-location-line (xref-item-location b))))) + (sort (progn ,@body) eglot-xref-sort-function) (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) (clrhash eglot--temp-location-buffers))) From aed8e9732b1cff2c9d24b0010ae47337afb072c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 11 Feb 2019 21:34:50 +0000 Subject: [PATCH 396/771] * eglot.el (xref-backend-references): don't use return-from. --- lisp/progmodes/eglot.el | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4fdd5c4e5be..df6a1b3ecb3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1767,24 +1767,24 @@ Try to visit the target file for a richer summary line." locations)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) - (unless (eglot--server-capable :referencesProvider) - (cl-return-from xref-backend-references nil)) - (let ((params - (or (get-text-property 0 :textDocumentPositionParams identifier) - (let ((rich (car (member identifier eglot--xref-known-symbols)))) - (and rich (get-text-property 0 :textDocumentPositionParams rich)))))) - (unless params - (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--handling-xrefs - (mapcar - (eglot--lambda ((Location) uri range) - (eglot--xref-make identifier uri range)) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/references - (append - params - (list :context - (list :includeDeclaration t)))))))) + (when (eglot--server-capable :referencesProvider) + (let ((params + (or (get-text-property 0 :textDocumentPositionParams identifier) + (let ((rich (car (member identifier eglot--xref-known-symbols)))) + (and rich + (get-text-property 0 :textDocumentPositionParams rich)))))) + (unless params + (eglot--error "Don' know where %s is in the workspace!" identifier)) + (eglot--handling-xrefs + (mapcar + (eglot--lambda ((Location) uri range) + (eglot--xref-make identifier uri range)) + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/references + (append + params + (list :context + (list :includeDeclaration t))))))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) From 80433528c2349a38aca5c2b884b71040978ead97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 13 Feb 2019 09:24:02 +0000 Subject: [PATCH 397/771] Unbreak build Messed up the name of eglot-xref-lessp-function. * eglot.el (eglot--handling-xrefs): Use eglot-xref-lessp-function GitHub-reference: per https://github.com/joaotavora/eglot/issues/220 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index df6a1b3ecb3..06600e2e971 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1684,7 +1684,7 @@ DUMMY is ignored." (defmacro eglot--handling-xrefs (&rest body) "Properly sort and handle xrefs produced and returned by BODY." `(unwind-protect - (sort (progn ,@body) eglot-xref-sort-function) + (sort (progn ,@body) eglot-xref-lessp-function) (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) (clrhash eglot--temp-location-buffers))) From 9383a2cd5c7fb9883c087725ccb00de712aad58a Mon Sep 17 00:00:00 2001 From: vjoki Date: Tue, 30 Apr 2019 12:35:24 +0300 Subject: [PATCH 398/771] Fix local function call in directory watcher () Copyright-paperwork-exempt: yes * eglot.el (eglot-register-capability workspace/didChangeWatchFiles): fix call to handle-event. GitHub-reference: https://github.com/joaotavora/eglot/issues/255 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 06600e2e971..ef0cb08342e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2367,8 +2367,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (changed 2) (deleted 3))))))) ((eq action 'renamed) - (handle-event desc 'deleted file) - (handle-event desc 'created file1)))))) + (handle-event '(desc 'deleted file)) + (handle-event '(desc 'created file1))))))) (unwind-protect (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) (push (file-notify-add-watch dir '(change) #'handle-event) From 68d95c8125327b7cef0ce496a70abb407bbf9b7f Mon Sep 17 00:00:00 2001 From: Michal Krzywkowski Date: Wed, 8 May 2019 13:33:34 +0200 Subject: [PATCH 399/771] Only consider eglot's own diagnostics in eglot-code-actions * eglot.el (eglot-code-actions): Filter out non-eglot diagnostics before sending a request to the server. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/260 --- lisp/progmodes/eglot.el | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ef0cb08342e..f5d81e4aa4c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2297,10 +2297,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :end (eglot--pos-to-lsp-position end)) :context `(:diagnostics - [,@(mapcar (lambda (diag) - (cdr (assoc 'eglot-lsp-diag - (eglot--diag-data diag)))) - (flymake-diagnostics beg end))])))) + [,@(cl-loop for diag in (flymake-diagnostics beg end) + when (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag))) + collect it)])))) (menu-items (or (mapcar (jsonrpc-lambda (&rest all &key title &allow-other-keys) (cons title all)) From c90f33dc212259220aea1cd9edf73cf1fe2c0826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 9 May 2019 19:49:31 +0100 Subject: [PATCH 400/771] Fix case when eglot-put-doc-in-help-buffer is nil * eglot.el (eglot--eldoc-message): Check eglot-put-doc-in-help-buffer. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/263 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f5d81e4aa4c..b9fc25c01e3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2075,7 +2075,8 @@ Buffer is displayed with `display-buffer', which obeys (when format (let ((string (apply #'format format args))) ;; FIXME: overworking? (when (or (eq t eglot-put-doc-in-help-buffer) - (funcall eglot-put-doc-in-help-buffer string)) + (and eglot-put-doc-in-help-buffer + (funcall eglot-put-doc-in-help-buffer string))) (with-current-buffer (eglot--help-buffer) (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) (let ((inhibit-read-only t)) From faa0500ff769e14365492ea6b1cc80c7e9d23725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 9 May 2019 11:38:58 +0100 Subject: [PATCH 401/771] Work around a bug in emacs's change detection When using capitalize-word, or any case-fiddling function, before-change-functions will record e.g. the whole word's start and end, even though only the first character has changed. Not only is this longer than needed but also conflicts with what we get in after-change-functions, which records just the one-char-long change. Also, if the word didn't need any fiddling at all then before-change-function will run but after-change-functions won't: an "orphan" before-change will erroneously be sent to the server. * eglot.el (eglot--after-change): Detect problematic case and fix change description. (eglot--before-change): Store markers of changed region. (eglot--signal-textDocument/didChange): Weed out orphan changes. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/259 --- lisp/progmodes/eglot.el | 52 +++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b9fc25c01e3..0c2f4e97bdd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1530,26 +1530,42 @@ THINGS are either registrations or unregisterations (sic)." (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") -(defun eglot--before-change (start end) - "Hook onto `before-change-functions' with START and END." - ;; Records START and END, crucially convert them into LSP - ;; (line/char) positions before that information is lost (because - ;; the after-change thingy doesn't know if newlines were - ;; deleted/added) +(defun eglot--before-change (beg end) + "Hook onto `before-change-functions' with BEG and END." (when (listp eglot--recent-changes) - (push `(,(eglot--pos-to-lsp-position start) - ,(eglot--pos-to-lsp-position end)) + ;; Records BEG and END, crucially convert them into LSP + ;; (line/char) positions before that information is lost (because + ;; the after-change thingy doesn't know if newlines were + ;; deleted/added). Also record markers of BEG and END + ;; (github#259) + (push `(,(eglot--pos-to-lsp-position beg) + ,(eglot--pos-to-lsp-position end) + (,beg . ,(copy-marker beg)) + (,end . ,(copy-marker end))) eglot--recent-changes))) -(defun eglot--after-change (start end pre-change-length) +(defun eglot--after-change (beg end pre-change-length) "Hook onto `after-change-functions'. -Records START, END and PRE-CHANGE-LENGTH locally." +Records BEG, END and PRE-CHANGE-LENGTH locally." (cl-incf eglot--versioned-identifier) - (if (and (listp eglot--recent-changes) - (null (cddr (car eglot--recent-changes)))) - (setf (cddr (car eglot--recent-changes)) - `(,pre-change-length ,(buffer-substring-no-properties start end))) - (setf eglot--recent-changes :emacs-messup)) + (pcase (and (listp eglot--recent-changes) + (car eglot--recent-changes)) + (`(,lsp-beg ,lsp-end + (,b-beg . ,b-beg-marker) + (,b-end . ,b-end-marker)) + ;; github#259: With `upcase-word' or somesuch, + ;; `before-change-functions' always records the whole word's + ;; `beg' and `end'. Not only is this longer than needed but + ;; conflicts with the args received here. Detect this using + ;; markers recorded earlier and `pre-change-len', then fix it. + (when (and (= b-end b-end-marker) (= b-beg b-beg-marker) + (not (zerop pre-change-length))) + (setq lsp-end (eglot--pos-to-lsp-position end) + lsp-beg (eglot--pos-to-lsp-position beg))) + (setcar eglot--recent-changes + `(,lsp-beg ,lsp-end ,pre-change-length + ,(buffer-substring-no-properties beg end)))) + (_ (setf eglot--recent-changes :emacs-messup))) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) (let ((buf (current-buffer))) (setq eglot--change-idle-timer @@ -1609,6 +1625,12 @@ When called interactively, use the currently active server" (buffer-substring-no-properties (point-min) (point-max))))) (cl-loop for (beg end len text) in (reverse eglot--recent-changes) + ;; github#259: `capitalize-word' and commands based + ;; on `casify_region' will cause multiple duplicate + ;; empty entries in `eglot--before-change' calls + ;; without an `eglot--after-change' reciprocal. + ;; Weed them out here. + when (numberp len) vconcat `[,(list :range `(:start ,beg :end ,end) :rangeLength len :text text)])))) (setq eglot--recent-changes nil) From 08d5a9dfde8b42407aa7cf81deef588270cb4a10 Mon Sep 17 00:00:00 2001 From: Akash Hiremath Date: Sun, 12 May 2019 16:17:37 +0530 Subject: [PATCH 402/771] Add built-in support for elixir's elixir-ls () * README.md: add elixir-ls. * eglot.el (eglot-server-programs): add elixir-ls. Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/264 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0c2f4e97bdd..ca637238810 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -99,7 +99,8 @@ language-server/bin/php-language-server.php")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) (java-mode . eglot--eclipse-jdt-contact) - (dart-mode . ("dart_language_server"))) + (dart-mode . ("dart_language_server")) + (elixir-mode . ("language_server.sh"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From 8f80ae14559389ae49843efb98bbb4d5ef09cf41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 27 Jun 2019 17:55:05 +0100 Subject: [PATCH 403/771] Leniently handle invalid positions sent by some servers * eglot.el (eglot--lsp-position-to-point): Leniently squash invalid character positions to 0. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/273 --- lisp/progmodes/eglot.el | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ca637238810..d4705551f47 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1020,8 +1020,14 @@ If optional MARKER, return a marker instead" (forward-line (min most-positive-fixnum (plist-get pos-plist :line))) (unless (eobp) ;; if line was excessive leave point at eob - (let ((tab-width 1)) - (funcall eglot-move-to-column-function (plist-get pos-plist :character)))) + (let ((tab-width 1) + (col (plist-get pos-plist :character))) + (unless (wholenump col) + (eglot--warn + :eglot "Caution: LSP server sent invalid character position %s. Using 0 instead." + col) + (setq col 0)) + (funcall eglot-move-to-column-function col))) (if marker (copy-marker (point-marker)) (point)))) (defun eglot--path-to-uri (path) From d79232df76b8fc583cf340703d448fb54bb1bbc3 Mon Sep 17 00:00:00 2001 From: Ingo Lohmar Date: Thu, 27 Jun 2019 18:56:45 +0200 Subject: [PATCH 404/771] Simplify eldoc usage () * eglot-tests.el (hover-after-completions): Protect test. Rewrite docstring. * eglot.el (eglot--managed-mode): Don't mess with eldoc-message-function. (eglot--eldoc-hint): Remove. (eglot--update-doc): Rename and rewrite from eglot--eldoc-message. (eglot-eldoc-function): Don't set eglot--eldoc-hint. Call eglot--update-doc. GitHub-reference: https://github.com/joaotavora/eglot/issues/269 --- lisp/progmodes/eglot.el | 63 +++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d4705551f47..aec975c1e6f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1172,7 +1172,6 @@ and just return it. PROMPT shouldn't end with a question mark." (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) - (add-function :before-until (local 'eldoc-message-function) #'eglot--eldoc-message) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) @@ -1189,7 +1188,6 @@ and just return it. PROMPT shouldn't end with a question mark." (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) - (remove-function (local 'eldoc-message-function) #'eglot--eldoc-message) (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) (setq eglot--current-flymake-report-fn nil)))) @@ -2050,8 +2048,6 @@ is not active." (buffer-string)))) when moresigs concat "\n")) -(defvar eglot--eldoc-hint nil) - (defvar eglot--help-buffer nil) (defun eglot--help-buffer () @@ -2100,29 +2096,30 @@ Buffer is displayed with `display-buffer', which obeys `display-buffer-alist' & friends." :type 'boolean) -(defun eglot--eldoc-message (format &rest args) - (when format - (let ((string (apply #'format format args))) ;; FIXME: overworking? - (when (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) - (rename-buffer (format "*eglot-help for %s*" eglot--eldoc-hint)) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert string) - (goto-char (point-min)) - (if eglot-auto-display-help-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - (eglot--message - "%s\n(...truncated. Full help is in `%s')" - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...") - (buffer-name eglot--help-buffer)))) - (help-mode) - t)))))) +(defun eglot--update-doc (string hint) + "Put updated documentation STRING where it belongs. +Honours `eglot-put-doc-in-help-buffer'. HINT is used to +potentially rename EGLOT's help buffer." + (if (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) + (rename-buffer (format "*eglot-help for %s*" hint)) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert string) + (goto-char (point-min)) + (if eglot-auto-display-help-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message + "%s\n(...truncated. Full help is in `%s')" + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...") + (buffer-name eglot--help-buffer)))) + (help-mode))) + (eldoc-message string))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function. @@ -2132,7 +2129,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (position-params (eglot--TextDocumentPositionParams)) sig-showing (thing-at-point (thing-at-point 'symbol))) - (setq eglot--eldoc-hint thing-at-point) (cl-macrolet ((when-buffer-window (&body body) ; notice the exception when testing with `ert' `(when (or (get-buffer-window buffer) (ert-running-test)) @@ -2146,10 +2142,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-buffer-window (when (cl-plusp (length signatures)) (setq sig-showing t) - (let ((eglot--eldoc-hint thing-at-point)) - (eldoc-message (eglot--sig-info signatures - activeSignature - activeParameter)))))) + (eglot--update-doc (eglot--sig-info signatures + activeSignature + activeParameter) + thing-at-point)))) :deferred :textDocument/signatureHelp)) (when (eglot--server-capable :hoverProvider) (jsonrpc-async-request @@ -2160,8 +2156,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (when-let (info (and contents (eglot--hover-info contents range))) - (let ((eglot--eldoc-hint thing-at-point)) - (eldoc-message info)))))) + (eglot--update-doc info thing-at-point))))) :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (jsonrpc-async-request From 6ed1f50cde74a4ae1a8225abbac37e6dd007da3f Mon Sep 17 00:00:00 2001 From: haqle314 <16577773+haqle314@users.noreply.github.com> Date: Tue, 2 Jul 2019 16:58:41 -0500 Subject: [PATCH 405/771] Fix a typo * eglot.el (eglot--lsp-position-to-point): fix eglot--warn call Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/273 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aec975c1e6f..25c8fbe7075 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1024,7 +1024,7 @@ If optional MARKER, return a marker instead" (col (plist-get pos-plist :character))) (unless (wholenump col) (eglot--warn - :eglot "Caution: LSP server sent invalid character position %s. Using 0 instead." + "Caution: LSP server sent invalid character position %s. Using 0 instead." col) (setq col 0)) (funcall eglot-move-to-column-function col))) From 3a9221c7b82ff7ce5d76ff1c791743255440d740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20H=C3=B6tzel?= Date: Thu, 18 Jul 2019 21:36:56 +0200 Subject: [PATCH 406/771] Fix invalid guess for php language server () * eglot.el (eglot-server-programs): Change the position of the php language server, otherwise it will always be hidden by the c-mode server (php-mode is derived from c-mode). Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/288 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 25c8fbe7075..a80be22e6ba 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -84,14 +84,14 @@ typescript-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) + (php-mode . ("php" "vendor/felixfbecker/\ +language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) . ("ocaml-language-server" "--stdio")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) - (php-mode . ("php" "vendor/felixfbecker/\ -language-server/bin/php-language-server.php")) (haskell-mode . ("hie-wrapper")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("go-langserver" "-mode=stdio" From f18137499d46ab4e2210d8190d09cea84834f8d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20H=C3=B6tzel?= Date: Mon, 12 Aug 2019 22:13:47 +0200 Subject: [PATCH 407/771] Expand directory watcher globs containing ** () Previously, if the server requested a glob pattern like foo/**/* to be watched, we would just error. Now we watch foo/bar/ and foo/baz/ as if the server had requested those two watchers instead of just the one with the **. As a limitation, the implementation of file-expand-wildcards doesn't fully handle ** globstars (** matches at most one path segment). * eglot.el (eglot-register-capability workspace/didChangeWatchedFiles): Use file-expand-wildcards to make a ** glob into multiple **-less globs. GitHub-reference: https://github.com/joaotavora/eglot/issues/293 --- lisp/progmodes/eglot.el | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a80be22e6ba..6078e97cdcd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2371,7 +2371,10 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (let* (success (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) globPattern) - watchers))) + watchers)) + (glob-dirs + (delete-dups (mapcar #'file-name-directory + (mapcan #'file-expand-wildcards globs))))) (cl-labels ((handle-event (event) @@ -2394,13 +2397,14 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (handle-event '(desc 'deleted file)) (handle-event '(desc 'created file1))))))) (unwind-protect - (progn (dolist (dir (delete-dups (mapcar #'file-name-directory globs))) - (push (file-notify-add-watch dir '(change) #'handle-event) - (gethash id (eglot--file-watches server)))) - (setq - success - `(:message ,(format "OK, watching %s watchers" - (length watchers))))) + (progn + (dolist (dir glob-dirs) + (push (file-notify-add-watch dir '(change) #'handle-event) + (gethash id (eglot--file-watches server)))) + (setq + success + `(:message ,(format "OK, watching %s directories in %s watchers" + (length glob-dirs) (length watchers))))) (unless success (eglot-unregister-capability server method id)))))) From 4a1d60dd6d6340b215d3f2b9376619cf92e8a2d8 Mon Sep 17 00:00:00 2001 From: David Florness Date: Sun, 18 Aug 2019 18:09:13 -0600 Subject: [PATCH 408/771] Require array package to use current-line () The jsonrpc package (one of eglot's dependencies) recently updated and removed the line requiring the array package. Since current-line is provided by array and is used by eglot, require array explicitly. Here's jsonrpc's guilty commit: https://git.savannah.gnu.org/cgit/emacs.git/commit/lisp/jsonrpc.el?id=c676444a43e4634c1f98ec286b5bd9e46b23216b Copyright-paperwork-exempt: Yes * eglot.el (array): Require it. GitHub-reference: https://github.com/joaotavora/eglot/issues/294 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6078e97cdcd..23d53edc2b2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -70,6 +70,7 @@ (require 'jsonrpc) (require 'filenotify) (require 'ert) +(require 'array) ;;; User tweakable stuff From e62b6395ee5284825abccb3bde4255ed894e0c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Tue, 10 Sep 2019 15:31:16 +0200 Subject: [PATCH 409/771] Change the default of eglot-move-to-column-function Previous default (move-to-column) works on visual columns, the LSP specification and the new default (eglot-move-to-column) use "real" columns. Fixes https://github.com/joaotavora/eglot/issues/293 and https://github.com/joaotavora/eglot/issues/297. * eglot.el (eglot-move-to-column): New function. (eglot-move-to-column-function): Use it as default. --- lisp/progmodes/eglot.el | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 23d53edc2b2..b87d5bb7c39 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -988,7 +988,7 @@ for all others.") :character (progn (when pos (goto-char pos)) (funcall eglot-current-column-function))))) -(defvar eglot-move-to-column-function #'move-to-column +(defvar eglot-move-to-column-function #'eglot-move-to-column "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based @@ -999,7 +999,16 @@ where X is a multi-byte character, it actually means `b', not For buffers managed by fully LSP-compliant servers, this should be set to `eglot-move-to-lsp-abiding-column', and -`move-to-column' (the default) for all others.") +`eglot-move-to-column' (the default) for all others.") + +(defun eglot-move-to-column (column) + "Move to COLUMN without closely following the LSP spec." + ;; We cannot use `move-to-column' here, because it moves to *visual* + ;; columns, which can be different from LSP columns in case of + ;; `whitespace-mode', `prettify-symbols-mode', etc. (github#296, + ;; github#297) + (goto-char (min (+ (line-beginning-position) column) + (line-end-position)))) (defun eglot-move-to-lsp-abiding-column (column) "Move to COLUMN abiding by the LSP spec." From 645bcfc6e57181c39dae1f238758e76c1759a765 Mon Sep 17 00:00:00 2001 From: Joram Schrijver Date: Fri, 13 Sep 2019 11:48:10 +0200 Subject: [PATCH 410/771] Treat null/nil server capabilities as false Some language servers may specify null for some capabilities in the list of server capabilities. This does not conform to the specification, but treating it as false is more reasonable than treating it as true. A current example is the PHP language server. which specifies null for every capability it does not handle, like documentHighlightProvider. This would cause Eglot to send constant textDocument/documentHighlight requests, which all timed out. * eglot.el (eglot--server-capable): Change the handling of null values for capabilities to treat them as false instead of true. Copyright-paperwork-exempt: yes --- lisp/progmodes/eglot.el | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 23d53edc2b2..845f0294dfa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1101,6 +1101,9 @@ under cursor." for probe = (plist-member caps feat) if (not probe) do (cl-return nil) if (eq (cadr probe) :json-false) do (cl-return nil) + ;; If the server specifies null as the value of the capability, it + ;; makes sense to treat it like false. + if (null (cadr probe)) do (cl-return nil) if (not (listp (cadr probe))) do (cl-return (if more nil (cadr probe))) finally (cl-return (or (cadr probe) t))))) From c53777030187d7273e05b84e242f2b3b5cfe513f Mon Sep 17 00:00:00 2001 From: Ingo Lohmar Date: Mon, 23 Sep 2019 20:05:55 +0200 Subject: [PATCH 411/771] Use gopls server as the default for go () Developers recommend it: see https://github.com/sourcegraph/go-langserver/blob/master/README.md * eglot (eglot-server-programs): Use gopls. * README.md: mention gopls instead of go-langserver. GitHub-reference: https://github.com/joaotavora/eglot/issues/304 --- lisp/progmodes/eglot.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b87d5bb7c39..d160b577008 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -95,8 +95,7 @@ language-server/bin/php-language-server.php")) :autoport)) (haskell-mode . ("hie-wrapper")) (kotlin-mode . ("kotlin-language-server")) - (go-mode . ("go-langserver" "-mode=stdio" - "-gocodecompletion")) + (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) (java-mode . eglot--eclipse-jdt-contact) From 051bc27a101f0c9f3376194ec53842c87d31e94e Mon Sep 17 00:00:00 2001 From: galeo Date: Thu, 26 Sep 2019 20:04:13 +0800 Subject: [PATCH 412/771] Also use signature label offsets for parameter info According to the LSP specification, a parameter of a callable-signature has a label and a optional doc-commet. The label of a parameter information is either a string or an inclusive start and exclusive end offsets within its containing signature label. Previously, this was only taken in account for highlighting the parameter in the definition signature. * eglot.el (eglot--sig-info): Handle signature label offsets when printing the signature parameter information. Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/272 --- lisp/progmodes/eglot.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3155135b304..723ac3bc89c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2055,7 +2055,10 @@ is not active." (goto-char (point-max)) (insert "\n" (propertize - label 'face 'eldoc-highlight-function-argument) + (if (stringp label) + label + (apply #'buffer-substring (mapcar #'1+ label))) + 'face 'eldoc-highlight-function-argument) ": " (eglot--format-markup documentation)))))) (buffer-string)))) when moresigs concat "\n")) From 14f69da41711f2826af60a511155b2d1a5025e4a Mon Sep 17 00:00:00 2001 From: Ingo Lohmar Date: Wed, 2 Oct 2019 18:03:48 +0200 Subject: [PATCH 413/771] On buffer kill, first send didclose then teardown local structures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It used to be the reverse way around, which doesn't make sense. * eglot.el (eglot-managed-mode): Fix order in `kill-buffer-hook' Co-authored-by: João Távora --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 723ac3bc89c..279fbeedad7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1172,8 +1172,9 @@ and just return it. PROMPT shouldn't end with a question mark." (eglot--managed-mode (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) - (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'kill-buffer-hook 'eglot--managed-mode-onoff nil t) + ;; Prepend "didClose" to the hook after the "onoff", so it will run first + (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) From ad1cc3b1c21188c5fe3264d6b4e2b1f93a9161b6 Mon Sep 17 00:00:00 2001 From: Ingo Lohmar Date: Wed, 2 Oct 2019 18:05:15 +0200 Subject: [PATCH 414/771] Optionally shutdown after killing last buffer of managed project () MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should close issue https://github.com/joaotavora/eglot/issues/217, also cf. https://github.com/joaotavora/eglot/issues/270. * eglot.el (eglot-autoshutdown): New defcustom. (eglot--managed-mode-onoff): Shutdown if so configured and no managed buffers left. Co-authored-by: João Távora GitHub-reference: https://github.com/joaotavora/eglot/issues/309 --- lisp/progmodes/eglot.el | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 279fbeedad7..cf2f371accd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -170,6 +170,10 @@ as 0, i.e. don't block at all." :type '(choice (boolean :tag "Whether to inhibit autoreconnection") (integer :tag "Number of seconds"))) +(defcustom eglot-autoshutdown nil + "If non-nil, shut down server after killing last managed buffer." + :type 'boolean) + (defcustom eglot-events-buffer-size 2000000 "Control the size of the Eglot events buffer. If a number, don't let the buffer grow larger than that many @@ -1224,7 +1228,11 @@ Reset in `eglot--managed-mode-onoff'.") (setq eglot--cached-current-server nil) (when server (setf (eglot--managed-buffers server) - (delq buf (eglot--managed-buffers server))))))))) + (delq buf (eglot--managed-buffers server))) + (when (and eglot-autoshutdown + (not (eglot--shutdown-requested server)) + (not (eglot--managed-buffers server))) + (eglot-shutdown server)))))))) (defun eglot--current-server () "Find the current logical EGLOT server." From 471434e068816b7d28616aeefa9fe53a5130eca1 Mon Sep 17 00:00:00 2001 From: Vladimir Panteleev Date: Fri, 4 Oct 2019 10:13:32 +0000 Subject: [PATCH 415/771] Don't send dummy json object in "initialized" notification () Eglot uses a JSON object { __dummy__ : true } as a placeholder instead of the empty object {}. It does this out of necessity, since encoding an empty object can't currently be easily using the current jsonrpc.el library. However, this also causes the parameter to be actually sent to the server. Since the JSON-RPC specification states "The names MUST match exactly, including case, to the method's expected parameters" this is non-conforming to the protocol. The LSP specification does not seem to indicate how servers should handle method calls with parameters they do not support. As such, ignoring the parameter, or reporting an error, or crashing all seem to be "valid" behaviors as far as the specification is concerned. We can avoid this by using an empty hash table instead of a dummy parameter. Currently, an empty hash table is the only Emacs Lisp object which jsonrpc.el serializes to an empty JSON object in jsonrpc--json-encode. * eglot.el (eglot--connect): Use make-hash-table instead of dummy object. Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/312 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cf2f371accd..ef7f8f0a3ec 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -848,7 +848,7 @@ This docstring appeases checkdoc, that's all." (push server (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) - (jsonrpc-notify server :initialized `(:__dummy__ t)) + (jsonrpc-notify server :initialized (make-hash-table)) (dolist (buffer (buffer-list)) (with-current-buffer buffer (eglot--maybe-activate-editing-mode server))) From fe37d7e3ce796a2bee21af6a0b34de59aef1daca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 Oct 2019 11:46:08 +0100 Subject: [PATCH 416/771] Revert "treat null/nil server capabilities as false" This reverts commit 645bcfc6e57181c39dae1f238758e76c1759a765. A capability of "null" is downright invalid, and must NOT be mistaken for a value of "{}" (which indicates the presence of the capability) or "False" (which indicates its asence). See https://github.com/microsoft/language-server-protocol/issues/830#issuecomment-537849292 for a clarification from the LSP maintainer. --- lisp/progmodes/eglot.el | 3 --- 1 file changed, 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ef7f8f0a3ec..d1a1a3d0b20 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1113,9 +1113,6 @@ under cursor." for probe = (plist-member caps feat) if (not probe) do (cl-return nil) if (eq (cadr probe) :json-false) do (cl-return nil) - ;; If the server specifies null as the value of the capability, it - ;; makes sense to treat it like false. - if (null (cadr probe)) do (cl-return nil) if (not (listp (cadr probe))) do (cl-return (if more nil (cadr probe))) finally (cl-return (or (cadr probe) t))))) From 0e7e66fe2739a203dd2f2e5138e27f8b8dd04fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 30 Sep 2019 18:06:48 +0200 Subject: [PATCH 417/771] Unbreak elm language server which does use :triggercharacters Only query completionProvider -> triggerCharacter information if the server has provided it. Elm's, and probaly other's, do not provide it, which doesn't mean they don't support completion. * eglot.el (eglot-completion-at-point): Check that completion capability is a list before treating it like one. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/285 --- lisp/progmodes/eglot.el | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d1a1a3d0b20..cfcfaa7d68a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1958,10 +1958,11 @@ is not active." :company-prefix-length (save-excursion (when (car bounds) (goto-char (car bounds))) - (looking-back - (regexp-opt - (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) - (line-beginning-position))) + (when (listp completion-capability) + (looking-back + (regexp-opt + (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) + (line-beginning-position)))) :exit-function (lambda (comp _status) (let ((comp (if (get-text-property 0 'eglot--lsp-completion comp) From 1c8d062c5f474ef33ffb056ab73cafaeaae2d7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 5 Oct 2019 12:32:23 +0100 Subject: [PATCH 418/771] Much less noisy mode line * eglot.el (eglot--mode-line-format): Simplify. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/236 --- lisp/progmodes/eglot.el | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cfcfaa7d68a..0a9da05b1c6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1314,7 +1314,7 @@ Uses THING, FACE, DEFS and PREPEND." (nick (and server (eglot--project-nickname server))) (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) - (`(,_id ,doing ,done-p ,detail) (and server (eglot--spinner server))) + (`(,_id ,doing ,done-p ,_detail) (and server (eglot--spinner server))) (last-error (and server (jsonrpc-last-error server)))) (append `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) @@ -1332,15 +1332,13 @@ Uses THING, FACE, DEFS and PREPEND." (format "An error occured: %s\n" (plist-get last-error :message))))) ,@(when (and doing (not done-p)) - `("/" ,(eglot--mode-line-props - (format "%s%s" doing - (if detail (format ":%s" detail) "")) - 'compilation-mode-line-run '()))) + `("/" ,(eglot--mode-line-props doing + 'compilation-mode-line-run '()))) ,@(when (cl-plusp pending) `("/" ,(eglot--mode-line-props - (format "%d outstanding requests" pending) 'warning + (format "%d" pending) 'warning '((mouse-3 eglot-forget-pending-continuations - "fahgettaboudit")))))))))) + "forget pending continuations")))))))))) (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) From 374ce4e29f1b025feed77aa2cb7e90a67864760e Mon Sep 17 00:00:00 2001 From: ambihelical Date: Sat, 11 May 2019 11:41:52 -0700 Subject: [PATCH 419/771] Allow user to set idle time to wait before processing changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot-send-changes-idle-time): New defcustom. (eglot--after-change): Use it. Co-authored-by: João Távora Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/258 --- lisp/progmodes/eglot.el | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a9da05b1c6..1dc57113984 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -174,6 +174,10 @@ as 0, i.e. don't block at all." "If non-nil, shut down server after killing last managed buffer." :type 'boolean) +(defcustom eglot-send-changes-idle-time 0.5 + "Don't tell server of changes before Emacs's been idle for this many seconds." + :type 'number) + (defcustom eglot-events-buffer-size 2000000 "Control the size of the Eglot events buffer. If a number, don't let the buffer grow larger than that many @@ -1591,10 +1595,11 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." (let ((buf (current-buffer))) (setq eglot--change-idle-timer (run-with-idle-timer - 0.5 nil (lambda () (eglot--with-live-buffer buf - (when eglot--managed-mode - (eglot--signal-textDocument/didChange) - (setq eglot--change-idle-timer nil)))))))) + eglot-send-changes-idle-time + nil (lambda () (eglot--with-live-buffer buf + (when eglot--managed-mode + (eglot--signal-textDocument/didChange) + (setq eglot--change-idle-timer nil)))))))) ;; HACK! Launching a deferred sync request with outstanding changes is a ;; bad idea, since that might lead to the request never having a From 83ed46b6a30ae4067df1436f6aef6f539adf5e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Thu, 3 Oct 2019 21:11:18 +0200 Subject: [PATCH 420/771] Support goto-{declaration, implementation, typedefinition} Closes https://github.com/joaotavora/eglot/issues/302. * eglot.el (eglot--xref-definitions-method): New variable. (xref-backend-definitions): Use it. (eglot-find-declaration, eglot-find-implementation, eglot-find-typeDefinition): New functions. * README.md (Language features): Add new capabilities. * eglot.el (eglot-client-capabilities): Add new capabilities. (eglot-ignored-server-capabilites): Add new capability. --- lisp/progmodes/eglot.el | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1dc57113984..e3ead963610 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -491,6 +491,9 @@ treated as in `eglot-dbind'." (:labelOffsetSupport t))) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) + :declaration `(:dynamicRegistration :json-false) + :implementation `(:dynamicRegistration :json-false) + :typeDefinition `(:dynamicRegistration :json-false) :documentSymbol (list :dynamicRegistration :json-false :symbolKind `(:valueSet @@ -1090,6 +1093,7 @@ under cursor." (const :tag "Go to definition" :definitionProvider) (const :tag "Go to type definition" :typeDefinitionProvider) (const :tag "Go to implementation" :implementationProvider) + (const :tag "Go to declaration" :implementationProvider) (const :tag "Find references" :referencesProvider) (const :tag "Highlight symbols automatically" :documentHighlightProvider) (const :tag "List symbols in buffer" :documentSymbolProvider) @@ -1796,6 +1800,36 @@ Try to visit the target file for a richer summary line." :textDocumentPositionParams (eglot--TextDocumentPositionParams)))) +(defvar eglot--xref-definitions-method :textDocument/definition + "The LSP method to map xref-find-definitions call.") + +(defun eglot-find-declaration () + "Find the declaration for the identifier at point. +See `xref-find-definitions' and `xref-prompt-for-identifier'." + (interactive) + (eglot--find-location 'declaration)) + +(defun eglot-find-implementation () + "Find the implementation for the identifier at point. +See `xref-find-definitions' and `xref-prompt-for-identifier'." + (interactive) + (eglot--find-location 'implementation)) + +(defun eglot-find-typeDefinition () + "Find the type definition for the identifier at point. +See `xref-find-definitions' and `xref-prompt-for-identifier'." + (interactive) + (eglot--find-location 'typeDefinition)) + +(defun eglot--find-location (kind) + (let* ((method-name (symbol-name kind)) + (method (intern (concat ":textDocument/" method-name))) + (capability (intern (concat ":" method-name "Provider")))) + (if (eglot--server-capable capability) + (let ((eglot--xref-definitions-method method)) + (call-interactively #'xref-find-definitions)) + (eglot--error "Server is not a %sProvider" method-name)))) + (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) (let* ((rich-identifier (car (member identifier eglot--xref-known-symbols))) @@ -1803,7 +1837,7 @@ Try to visit the target file for a richer summary line." (if rich-identifier (get-text-property 0 :locations rich-identifier) (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/definition + eglot--xref-definitions-method (get-text-property 0 :textDocumentPositionParams identifier)))) (locations From 9bb0331d04635002972b5208db6394ad931a4b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 6 Oct 2019 16:10:33 +0100 Subject: [PATCH 421/771] Rework and correct major part of xref glue code See comments of https://github.com/joaotavora/eglot/pull/314. Up to now, xref-backend-indentifier-completion-table was a gross hack that only worked sometimes. It relied on some fugly gymnastics to cache a response from :textDocument/documentSymbol and somehow used that information to build a completion table. But it doesn't work well. Summarily, LSP doesn't lend itself well to the xref interface of prompting for an arbitrary identifier and then go look for whichever type of references of that identifier. All the LSP :textDocument/{definition,references,implementation,...} methods expect to know the exact context of the search the user is about to perform, in the form of a document location. That conflicts with the xref "arbitrary string" requirement. Therefore, the slightly limited, but much more correct way, for Eglot to function is to override the user's preference of xref-prompt-for-identifier, temporarily setting it to nil in eglot--managed-mode (ideally, though, xref-prompt-for-identifier should be a function of the backend.) Later on, a possibly better behaved identifier completion table can be built on top of the :workspace/symbol LSP method. * eglot.el (xref-backend-identifier-at-point): Rewrite. (eglot--lsp-xrefs-for-method): New helper. (eglot--lsp-xref-helper): Use eglot--lsp-xrefs-for-method. (eglot--xref-definitions-method): Delete. (eglot--lsp-xref-refs): New variable. (xref-backend-references, xref-backend-definitions): Use eglot--lsp-xrefs-for-method. (eglot--managed-mode): Set xref-prompt-for-identifier to nil. (eglot--xref-reset-known-symbols, eglot--xref-known-symbols): Delete (xref-backend-identifier-completion-table): Nullify. (eglot-find-declaration, eglot-find-implementation) (eglot-find-typeDefinition): Use eglot--lsp-xref-helper. --- lisp/progmodes/eglot.el | 137 +++++++++++++--------------------------- 1 file changed, 45 insertions(+), 92 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e3ead963610..51914d9e4ed 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1193,6 +1193,7 @@ and just return it. PROMPT shouldn't end with a question mark." (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 xref-prompt-for-identifier nil) (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) @@ -1717,16 +1718,6 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." "EGLOT xref backend." (when (eglot--server-capable :definitionProvider) 'eglot)) -(defvar eglot--xref-known-symbols nil) - -(defun eglot--xref-reset-known-symbols (&rest _dummy) - "Reset `eglot--xref-reset-known-symbols'. -DUMMY is ignored." - (setq eglot--xref-known-symbols nil)) - -(advice-add 'xref-find-definitions :after #'eglot--xref-reset-known-symbols) -(advice-add 'xref-find-references :after #'eglot--xref-reset-known-symbols) - (defvar eglot--temp-location-buffers (make-hash-table :test #'equal) "Helper variable for `eglot--handling-xrefs'.") @@ -1771,102 +1762,64 @@ Try to visit the target file for a richer summary line." (xref-make summary (xref-make-file-location file line column)))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (when (eglot--server-capable :documentSymbolProvider) - (let ((server (eglot--current-server-or-lose)) - (text-id (eglot--TextDocumentIdentifier))) - (completion-table-with-cache - (lambda (string) - (setq eglot--xref-known-symbols - (mapcar - (eglot--lambda - ((SymbolInformation) name kind location containerName) - (propertize name - :textDocumentPositionParams - (list :textDocument text-id - :position (plist-get - (plist-get location :range) - :start)) - :locations (vector location) - :kind kind - :containerName containerName)) - (jsonrpc-request server - :textDocument/documentSymbol - `(:textDocument ,text-id)))) - (all-completions string eglot--xref-known-symbols)))))) + (eglot--error "cannot (yet) provide reliable completion table for LSP symbols")) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) - (when-let ((symatpt (symbol-at-point))) - (propertize (symbol-name symatpt) - :textDocumentPositionParams - (eglot--TextDocumentPositionParams)))) + ;; JT@19/10/09: This is a totally dummy identifier that isn't even + ;; passed to LSP. The reason for this particular wording is to + ;; construct a readable message "No references for LSP identifier at + ;; point.". See http://github.com/joaotavora/eglot/issues/314 + "LSP identifier at point.") -(defvar eglot--xref-definitions-method :textDocument/definition - "The LSP method to map xref-find-definitions call.") +(defvar eglot--lsp-xref-refs nil + "`xref' objects for overriding `xref-backend-references''s.") + +(cl-defun eglot--lsp-xrefs-for-method (method &key extra-params capability) + "Make `xref''s for METHOD, EXTRA-PARAMS, check CAPABILITY." + (unless (eglot--server-capable + (or capability + (intern + (format ":%sProvider" + (cadr (split-string (symbol-name method) + "/")))))) + (eglot--error "Sorry, this server doesn't do %s" method)) + (eglot--handling-xrefs + (mapcar + (eglot--lambda ((Location) uri range) + (eglot--xref-make (symbol-at-point) uri range)) + (jsonrpc-request + (eglot--current-server-or-lose) method (append + (eglot--TextDocumentPositionParams) + extra-params))))) + +(defun eglot--lsp-xref-helper (method) + "Helper for `eglot-find-declaration' & friends." + (let ((eglot--lsp-xref-refs (eglot--lsp-xrefs-for-method method))) + (xref-find-references "LSP identifier at point."))) (defun eglot-find-declaration () - "Find the declaration for the identifier at point. -See `xref-find-definitions' and `xref-prompt-for-identifier'." + "Find declaration for SYM, the identifier at point." (interactive) - (eglot--find-location 'declaration)) + (eglot--lsp-xref-helper :textDocument/declaration)) (defun eglot-find-implementation () - "Find the implementation for the identifier at point. -See `xref-find-definitions' and `xref-prompt-for-identifier'." + "Find implementation for SYM, the identifier at point." (interactive) - (eglot--find-location 'implementation)) + (eglot--lsp-xref-helper :textDocument/implementation)) (defun eglot-find-typeDefinition () - "Find the type definition for the identifier at point. -See `xref-find-definitions' and `xref-prompt-for-identifier'." + "Find type definition for SYM, the identifier at point." (interactive) - (eglot--find-location 'typeDefinition)) + (eglot--lsp-xref-helper :textDocument/typeDefinition)) -(defun eglot--find-location (kind) - (let* ((method-name (symbol-name kind)) - (method (intern (concat ":textDocument/" method-name))) - (capability (intern (concat ":" method-name "Provider")))) - (if (eglot--server-capable capability) - (let ((eglot--xref-definitions-method method)) - (call-interactively #'xref-find-definitions)) - (eglot--error "Server is not a %sProvider" method-name)))) +(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) _identifier) + (eglot--lsp-xrefs-for-method :textDocument/definition)) -(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) identifier) - (let* ((rich-identifier - (car (member identifier eglot--xref-known-symbols))) - (definitions - (if rich-identifier - (get-text-property 0 :locations rich-identifier) - (jsonrpc-request (eglot--current-server-or-lose) - eglot--xref-definitions-method - (get-text-property - 0 :textDocumentPositionParams identifier)))) - (locations - (and definitions - (if (vectorp definitions) definitions (vector definitions))))) - (eglot--handling-xrefs - (mapcar (eglot--lambda ((Location) uri range) - (eglot--xref-make identifier uri range)) - locations)))) - -(cl-defmethod xref-backend-references ((_backend (eql eglot)) identifier) - (when (eglot--server-capable :referencesProvider) - (let ((params - (or (get-text-property 0 :textDocumentPositionParams identifier) - (let ((rich (car (member identifier eglot--xref-known-symbols)))) - (and rich - (get-text-property 0 :textDocumentPositionParams rich)))))) - (unless params - (eglot--error "Don' know where %s is in the workspace!" identifier)) - (eglot--handling-xrefs - (mapcar - (eglot--lambda ((Location) uri range) - (eglot--xref-make identifier uri range)) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/references - (append - params - (list :context - (list :includeDeclaration t))))))))) +(cl-defmethod xref-backend-references ((_backend (eql eglot)) _identifier) + (or + eglot--lsp-xref-refs + (eglot--lsp-xrefs-for-method + :textDocument/references :extra-params `(:context (:includeDeclaration t))))) (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) From c848af80d5fc58bf5560a87c192366e467abf56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 9 Oct 2019 19:30:27 +0100 Subject: [PATCH 422/771] Misc improvements to the xref glue code * eglot.el (eglot-xref-backend): Don't check capability here. (eglot--collecting-xrefs): Reworked from eglot--handling-xrefs. (eglot--handling-xrefs): Remove. (xref-backend-apropos, eglot--lsp-xrefs-for-method): Use eglot--collecting-xrefs. --- lisp/progmodes/eglot.el | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 51914d9e4ed..9d5c546cefe 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1714,9 +1714,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (funcall report-fn (cdr eglot--unreported-diagnostics)) (setq eglot--unreported-diagnostics nil))) -(defun eglot-xref-backend () - "EGLOT xref backend." - (when (eglot--server-capable :definitionProvider) 'eglot)) +(defun eglot-xref-backend () "EGLOT xref backend." 'eglot) (defvar eglot--temp-location-buffers (make-hash-table :test #'equal) "Helper variable for `eglot--handling-xrefs'.") @@ -1724,12 +1722,16 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (defvar eglot-xref-lessp-function #'ignore "Compare two `xref-item' objects for sorting.") -(defmacro eglot--handling-xrefs (&rest body) - "Properly sort and handle xrefs produced and returned by BODY." - `(unwind-protect - (sort (progn ,@body) eglot-xref-lessp-function) - (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) - (clrhash eglot--temp-location-buffers))) +(cl-defmacro eglot--collecting-xrefs ((collector) &rest body) + "Sort and handle xrefs collected with COLLECTOR in BODY." + (let ((collected (cl-gensym "collected"))) + `(unwind-protect + (let (,collected) + (cl-flet ((,collector (xref) (push xref ,collected))) + ,@body) + (sort ,collected eglot-xref-lessp-function)) + (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) + (clrhash eglot--temp-location-buffers)))) (defun eglot--xref-make (name uri range) "Like `xref-make' but with LSP's NAME, URI and RANGE. @@ -1783,8 +1785,8 @@ Try to visit the target file for a richer summary line." (cadr (split-string (symbol-name method) "/")))))) (eglot--error "Sorry, this server doesn't do %s" method)) - (eglot--handling-xrefs - (mapcar + (eglot--collecting-xrefs (collect) + (mapc (eglot--lambda ((Location) uri range) (eglot--xref-make (symbol-at-point) uri range)) (jsonrpc-request @@ -1792,9 +1794,12 @@ Try to visit the target file for a richer summary line." (eglot--TextDocumentPositionParams) extra-params))))) -(defun eglot--lsp-xref-helper (method) +(cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) "Helper for `eglot-find-declaration' & friends." - (let ((eglot--lsp-xref-refs (eglot--lsp-xrefs-for-method method))) + (let ((eglot--lsp-xref-refs (eglot--lsp-xrefs-for-method + method + :extra-params extra-params + :capability capability))) (xref-find-references "LSP identifier at point."))) (defun eglot-find-declaration () @@ -1823,11 +1828,11 @@ Try to visit the target file for a richer summary line." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) - (eglot--handling-xrefs - (mapcar + (eglot--collecting-xrefs (collect) + (mapc (eglot--lambda ((SymbolInformation) name location) (eglot--dbind ((Location) uri range) location - (eglot--xref-make name uri range))) + (collect (eglot--xref-make name uri range)))) (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol `(:query ,pattern)))))) From 0aaaea5ae9be07e979cbcd636226a088c1650017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 13 Oct 2019 22:32:52 +0100 Subject: [PATCH 423/771] Unbreak xref-find-definitions * eglot-tests.el (basic-xref): New test. * eglot.el (eglot--collecting-xrefs): Add an edebug spec. (eglot--lsp-xrefs-for-method): Actually collect xref. (xref-backend-apropos): Fix indentation slightly. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/318 --- lisp/progmodes/eglot.el | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9d5c546cefe..7b0e3e28a6d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1724,6 +1724,7 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (cl-defmacro eglot--collecting-xrefs ((collector) &rest body) "Sort and handle xrefs collected with COLLECTOR in BODY." + (declare (indent 1) (debug (sexp &rest form))) (let ((collected (cl-gensym "collected"))) `(unwind-protect (let (,collected) @@ -1786,13 +1787,13 @@ Try to visit the target file for a richer summary line." "/")))))) (eglot--error "Sorry, this server doesn't do %s" method)) (eglot--collecting-xrefs (collect) - (mapc - (eglot--lambda ((Location) uri range) - (eglot--xref-make (symbol-at-point) uri range)) - (jsonrpc-request - (eglot--current-server-or-lose) method (append - (eglot--TextDocumentPositionParams) - extra-params))))) + (mapc + (eglot--lambda ((Location) uri range) + (collect (eglot--xref-make (symbol-at-point) uri range))) + (jsonrpc-request + (eglot--current-server-or-lose) method (append + (eglot--TextDocumentPositionParams) + extra-params))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) "Helper for `eglot-find-declaration' & friends." @@ -1829,13 +1830,13 @@ Try to visit the target file for a richer summary line." (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) (when (eglot--server-capable :workspaceSymbolProvider) (eglot--collecting-xrefs (collect) - (mapc - (eglot--lambda ((SymbolInformation) name location) - (eglot--dbind ((Location) uri range) location - (collect (eglot--xref-make name uri range)))) - (jsonrpc-request (eglot--current-server-or-lose) - :workspace/symbol - `(:query ,pattern)))))) + (mapc + (eglot--lambda ((SymbolInformation) name location) + (eglot--dbind ((Location) uri range) location + (collect (eglot--xref-make name uri range)))) + (jsonrpc-request (eglot--current-server-or-lose) + :workspace/symbol + `(:query ,pattern)))))) (defun eglot-format-buffer () "Format contents of current buffer." From c2e084bc23f3e717605d54b334ead0816f6d445d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 12 Oct 2019 01:57:00 +0100 Subject: [PATCH 424/771] Fix eglot-completion-at-point to work with bare completion-at-point Fixes https://github.com/joaotavora/eglot/issues/313, fixes https://github.com/joaotavora/eglot/issues/311, fixes https://github.com/joaotavora/eglot/issues/279 As is well known, LSP's and Emacs's completion mechanics don't fit very well together, mostly because Emacs expects completion to be a something of a pure function of a string argument and LSP treats as a function of a concrete buffer position. A further complication arises because some completion frontends like "bare" completion-at-point make Emacs modify the buffer's contents during the completion process, while other (notably company-mode) don't do that. Thus, 'eglot-completion-at-point' must take extra care to answer to the questions listed in the "(elisp)Programmed Completion" info node based on its (quite hacky) "completions" local var and _not_ based on the intermediate buffer contents. That var is also used to cache the last LSP response and allow the :exit-function callback to retrieve much more than just the completion text in In yet another related problem, :exit-function won't be called at all with completion-at-point if the completion table doesn't answer properly to test-completion. A previous use of completion-table-dynamic was found to be unsuitable here: we must answer all the requests separately. * eglot.el (eglot-completion-at-point): Rework. --- lisp/progmodes/eglot.el | 60 +++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7b0e3e28a6d..421941f1955 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1883,38 +1883,40 @@ is not active." (or (get-text-property 0 :sortText a) "") (or (get-text-property 0 :sortText b) "")))))) (metadata `(metadata . ((display-sort-function . ,sort-completions)))) - strings) + completions) (when completion-capability (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (lambda (string pred action) - (if (eq action 'metadata) metadata - (funcall - (completion-table-dynamic - (lambda (_ignored) - (let* ((resp (jsonrpc-request server - :textDocument/completion - (eglot--CompletionParams) - :deferred :textDocument/completion - :cancel-on-input t)) - (items (if (vectorp resp) resp (plist-get resp :items)))) - (setq - strings - (mapcar - (jsonrpc-lambda - (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (put-text-property 0 1 'eglot--lsp-completion all completion) - completion)) - items))))) - string pred action))) + (lambda (comp _pred action) + (cond + ((eq action 'metadata) metadata) ; metadata + ((eq action 'lambda) (member comp completions)) ; test-completion + ((eq (car-safe action) 'boundaries) nil) ; boundaries + ((and (null action) (member comp completions) t)) ; try-completion + ((eq action t) ; all-completions + (let* ((resp (jsonrpc-request server + :textDocument/completion + (eglot--CompletionParams) + :deferred :textDocument/completion + :cancel-on-input t)) + (items (if (vectorp resp) resp (plist-get resp :items)))) + (setq + completions + (mapcar + (jsonrpc-lambda + (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (put-text-property 0 1 'eglot--lsp-completion + all completion) + completion)) + items)))))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) @@ -1966,7 +1968,7 @@ is not active." ;; When selecting from the *Completions* ;; buffer, `comp' won't have any properties. A ;; lookup should fix that (github#148) - (cl-find comp strings :test #'string=)))) + (cl-find comp completions :test #'string=)))) (eglot--dbind ((CompletionItem) insertTextFormat insertText textEdit From 471fff254f6babaad608da4383f6d94be10e665e Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Tue, 15 Oct 2019 10:32:57 -0600 Subject: [PATCH 425/771] Add support for the ada language server * eglot.el (eglot-server-programs): Add ada-mode entry. * README.md (Connecting to a server): Add Ada entry. GitHub-reference: close https://github.com/joaotavora/eglot/issues/316 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 421941f1955..970f024db1b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -100,7 +100,8 @@ language-server/bin/php-language-server.php")) "languageserver::run()")) (java-mode . eglot--eclipse-jdt-contact) (dart-mode . ("dart_language_server")) - (elixir-mode . ("language_server.sh"))) + (elixir-mode . ("language_server.sh")) + (ada-mode . ("ada_language_server"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From 320356985a9315453c3551ee835a22979c543dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 15 Oct 2019 18:42:33 +0100 Subject: [PATCH 426/771] Fix bug in workspace/didchangewatchedfiles * eglot.el (eglot-register-capability): Fix a bug and a couple of warnings. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 970f024db1b..f757a8ac4ff 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2415,8 +2415,8 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (changed 2) (deleted 3))))))) ((eq action 'renamed) - (handle-event '(desc 'deleted file)) - (handle-event '(desc 'created file1))))))) + (handle-event `(,desc 'deleted ,file)) + (handle-event `(,desc 'created ,file1))))))) (unwind-protect (progn (dolist (dir glob-dirs) From ca9649c6b39abe75e90822936b214d6d4eb10ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Oct 2019 09:49:09 +0100 Subject: [PATCH 427/771] Always filter completions client-side by prefix Prefix completion is all we get in LSP because there are some servers that send *all* completions everytime. This is horrible, but it's the currently defined behaviour. See https://github.com/microsoft/language-server-protocol/issues/651. * eglot.el (eglot-completion-at-point): Use all-completions. GitHub-reference: per https://github.com/joaotavora/eglot/issues/319 --- lisp/progmodes/eglot.el | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f757a8ac4ff..ce3705a3a36 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1889,7 +1889,7 @@ is not active." (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (lambda (comp _pred action) + (lambda (comp pred action) (cond ((eq action 'metadata) metadata) ; metadata ((eq action 'lambda) (member comp completions)) ; test-completion @@ -1904,20 +1904,23 @@ is not active." (items (if (vectorp resp) resp (plist-get resp :items)))) (setq completions - (mapcar - (jsonrpc-lambda - (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (put-text-property 0 1 'eglot--lsp-completion - all completion) - completion)) - items)))))) + (all-completions ; <-stuck with prefix-comp because LSP + comp + (mapcar + (jsonrpc-lambda + (&rest all &key label insertText insertTextFormat + &allow-other-keys) + (let ((completion + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (put-text-property 0 1 'eglot--lsp-completion + all completion) + completion)) + items) + pred)))))) :annotation-function (lambda (obj) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) From c85ee68e29a986aab4c6c3a825cd40506667b884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Oct 2019 14:16:52 +0100 Subject: [PATCH 428/771] Play along with lsp's filtertext hacks Reworked important parts of eglot-completion-at-point. One of the tasks was to cleanup the nomenclature so it's easier to spot how LSP and Emacs's views of completion techniques differ. When reading this rather long function, remember an "item" is a plist representing the LSP completionItem object, and "proxy" is a propertized string that Emacs's frontends will use to represent that completion. When the completion is close to done, the :exit-function is called, to potentially rework the inserted text so that the final result might be quite different from the proxy (it might be a snippet, or even a suprising text edit). The most important change in this commit reworks the way the completion "bounds" are calculated in the buffer. This is the region that Emacs needs to know that is being targeted for the completion. A server can specify this region by using textEdit-based completions all consistently pointing to the same range. If it does so, Emacs will use that region instead of its own understanding of symbol boundaries (provided by thingatpt.el and syntax tables). To implement server-side completion filtering, the server can also provide a filterText "cookie" in each completion, which, when prefix-matched to the intended region, selects or rejects the completion. Given the feedback in https://github.com/microsoft/language-server-protocol/issues/651, we have no choice but to play along with that inneficient and grotesque strategy to implement flex-style matching. Like ever in LSP, we do so while being backward-compatible to all previously supported behaviour. * eglot.el (eglot-completion-at-point): rework. GitHub-reference: close https://github.com/joaotavora/eglot/issues/235 --- lisp/progmodes/eglot.el | 186 +++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 88 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ce3705a3a36..9460e4b3c64 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1874,57 +1874,69 @@ is not active." (defun eglot-completion-at-point () "EGLOT's `completion-at-point' function." - (let* ((bounds (bounds-of-thing-at-point 'symbol)) - (server (eglot--current-server-or-lose)) - (completion-capability (eglot--server-capable :completionProvider)) - (sort-completions (lambda (completions) - (sort completions - (lambda (a b) - (string-lessp - (or (get-text-property 0 :sortText a) "") - (or (get-text-property 0 :sortText b) "")))))) - (metadata `(metadata . ((display-sort-function . ,sort-completions)))) - completions) - (when completion-capability + ;; Commit logs for this function help understand what's going on. + (when-let (completion-capability (eglot--server-capable :completionProvider)) + (let* ((server (eglot--current-server-or-lose)) + (sort-completions (lambda (completions) + (sort completions + (lambda (a b) + (string-lessp + (or (get-text-property 0 :sortText a) "") + (or (get-text-property 0 :sortText b) "")))))) + (metadata `(metadata . ((display-sort-function . ,sort-completions)))) + (response (jsonrpc-request server + :textDocument/completion + (eglot--CompletionParams) + :deferred :textDocument/completion + :cancel-on-input t)) + (items (append ; coerce to list + (if (vectorp response) response (plist-get response :items)) + nil)) + (proxies + (mapcar (jsonrpc-lambda + (&rest item &key label insertText insertTextFormat + &allow-other-keys) + (let ((proxy + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (put-text-property 0 1 'eglot--lsp-item item proxy) + proxy)) + items)) + (bounds + (cl-loop with probe = + (plist-get (plist-get (car items) :textEdit) :range) + for item in (cdr items) + for range = (plist-get (plist-get item :textEdit) :range) + unless (and range (equal range probe)) + return (bounds-of-thing-at-point 'symbol) + finally (cl-return (or (and probe + (eglot--range-region probe)) + (bounds-of-thing-at-point 'symbol)))))) (list (or (car bounds) (point)) (or (cdr bounds) (point)) - (lambda (comp pred action) + (lambda (probe pred action) (cond - ((eq action 'metadata) metadata) ; metadata - ((eq action 'lambda) (member comp completions)) ; test-completion - ((eq (car-safe action) 'boundaries) nil) ; boundaries - ((and (null action) (member comp completions) t)) ; try-completion - ((eq action t) ; all-completions - (let* ((resp (jsonrpc-request server - :textDocument/completion - (eglot--CompletionParams) - :deferred :textDocument/completion - :cancel-on-input t)) - (items (if (vectorp resp) resp (plist-get resp :items)))) - (setq - completions - (all-completions ; <-stuck with prefix-comp because LSP - comp - (mapcar - (jsonrpc-lambda - (&rest all &key label insertText insertTextFormat - &allow-other-keys) - (let ((completion - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (put-text-property 0 1 'eglot--lsp-completion - all completion) - completion)) - items) - pred)))))) + ((eq action 'metadata) metadata) ; metadata + ((eq action 'lambda) (member probe proxies)) ; test-completion + ((eq (car-safe action) 'boundaries) nil) ; boundaries + ((and (null action) (member probe proxies) t)) ; try-completion + ((eq action t) ; all-completions + (cl-remove-if-not + (lambda (proxy) + (let* ((item (get-text-property 0 'eglot--lsp-item proxy)) + (filterText (plist-get item :filterText))) + (and (or (null pred) (funcall pred proxy)) + (string-prefix-p + probe (or filterText proxy) completion-ignore-case)))) + proxies)))) :annotation-function - (lambda (obj) + (lambda (proxy) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) - (get-text-property 0 'eglot--lsp-completion obj) + (get-text-property 0 'eglot--lsp-item proxy) (let* ((detail (and (stringp detail) (not (string= detail "")) detail)) @@ -1939,10 +1951,9 @@ is not active." (eglot--snippet-expansion-fn) " (snippet)")))))) :company-doc-buffer - (lambda (obj) + (lambda (proxy) (let* ((documentation - (let ((lsp-comp - (get-text-property 0 'eglot--lsp-completion obj))) + (let ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy))) (or (plist-get lsp-comp :documentation) (and (eglot--server-capable :completionProvider :resolveProvider) @@ -1966,46 +1977,45 @@ is not active." (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) (line-beginning-position)))) :exit-function - (lambda (comp _status) - (let ((comp (if (get-text-property 0 'eglot--lsp-completion comp) - comp - ;; When selecting from the *Completions* - ;; buffer, `comp' won't have any properties. A - ;; lookup should fix that (github#148) - (cl-find comp completions :test #'string=)))) - (eglot--dbind ((CompletionItem) insertTextFormat - insertText - textEdit - additionalTextEdits) - (get-text-property 0 'eglot--lsp-completion comp) - (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (cond (textEdit - ;; Undo the just the completed bit. If before - ;; completion the buffer was "foo.b" and now is - ;; "foo.bar", `comp' will be "bar". We want to - ;; delete only "ar" (`comp' minus the symbol - ;; whose bounds we've calculated before) - ;; (github#160). - (delete-region (+ (- (point) (length comp)) - (if bounds (- (cdr bounds) (car bounds)) 0)) - (point)) - (eglot--dbind ((TextEdit) range newText) textEdit - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (delete-region beg end) - (goto-char beg) - (funcall (or snippet-fn #'insert) newText))) - (when (cl-plusp (length additionalTextEdits)) - (eglot--apply-text-edits additionalTextEdits))) - (snippet-fn - ;; A snippet should be inserted, but using plain - ;; `insertText'. This requires us to delete the - ;; whole completion, since `insertText' is the full - ;; completion's text. - (delete-region (- (point) (length comp)) (point)) - (funcall snippet-fn insertText)))) - (eglot--signal-textDocument/didChange) - (eglot-eldoc-function)))))))) + (lambda (proxy _status) + (eglot--dbind ((CompletionItem) insertTextFormat + insertText + textEdit + additionalTextEdits) + (or (get-text-property 0 'eglot--lsp-item proxy) + ;; When selecting from the *Completions* + ;; buffer, `proxy' won't have any properties. A + ;; lookup should fix that (github#148) + (get-text-property + 0 'eglot--lsp-item (cl-find proxy proxies :test #'string=))) + (let ((snippet-fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (cond (textEdit + ;; Undo (yes, undo) the newly inserted completion. + ;; If before completion the buffer was "foo.b" and + ;; now is "foo.bar", `proxy' will be "bar". We + ;; want to delete only "ar" (`proxy' minus the + ;; symbol whose bounds we've calculated before) + ;; (github#160). + (delete-region (+ (- (point) (length proxy)) + (if bounds (- (cdr bounds) (car bounds)) 0)) + (point)) + (eglot--dbind ((TextEdit) range newText) textEdit + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (delete-region beg end) + (goto-char beg) + (funcall (or snippet-fn #'insert) newText))) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) + (snippet-fn + ;; A snippet should be inserted, but using plain + ;; `insertText'. This requires us to delete the + ;; whole completion, since `insertText' is the full + ;; completion's text. + (delete-region (- (point) (length proxy)) (point)) + (funcall snippet-fn insertText)))) + (eglot--signal-textDocument/didChange) + (eglot-eldoc-function))))))) (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") From 024bbfc6163acbd2545f74d4537f801ae7bf012f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Oct 2019 16:29:41 +0100 Subject: [PATCH 429/771] Use of company-capf backend in eglot-managed buffers * eglot.el (company-backends): forward-declare (eglot--managed-mode): Force company-backends to company-capf --- lisp/progmodes/eglot.el | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9460e4b3c64..0220969e6fd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -71,6 +71,8 @@ (require 'filenotify) (require 'ert) (require 'array) +(defvar company-backends) ; forward-declare, but don't require company yet + ;;; User tweakable stuff @@ -1196,6 +1198,7 @@ and just return it. PROMPT shouldn't end with a question mark." (eglot--setq-saving eldoc-documentation-function #'eglot-eldoc-function) (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)) (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) From 1aa5d0b528996dd56ab20a04235d3cd032c6e616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Oct 2019 16:46:03 +0100 Subject: [PATCH 430/771] Unbreak eglot--setq-saving if symbol is unbound * eglot.el (eglot--setq-saving): check if symbol is bound --- lisp/progmodes/eglot.el | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0220969e6fd..bb5d5f81690 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1173,9 +1173,10 @@ and just return it. PROMPT shouldn't end with a question mark." "Bindings saved by `eglot--setq-saving'.") (defmacro eglot--setq-saving (symbol binding) - `(progn (push (cons ',symbol (symbol-value ',symbol)) - eglot--saved-bindings) - (setq-local ,symbol ,binding))) + `(when (boundp ',symbol) + (push (cons ',symbol (symbol-value ',symbol)) + eglot--saved-bindings) + (setq-local ,symbol ,binding))) (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." From 5d1c5c64b17a127daa0e69a14bce1e68362f36ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Oct 2019 19:25:51 +0100 Subject: [PATCH 431/771] Don't choke on single-location reply to td/definition * eglot.el (eglot--lsp-xrefs-for-method): Accept non-vector Location. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/321 --- lisp/progmodes/eglot.el | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bb5d5f81690..aa64fe6a2a9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1791,14 +1791,15 @@ Try to visit the target file for a richer summary line." (cadr (split-string (symbol-name method) "/")))))) (eglot--error "Sorry, this server doesn't do %s" method)) - (eglot--collecting-xrefs (collect) - (mapc - (eglot--lambda ((Location) uri range) - (collect (eglot--xref-make (symbol-at-point) uri range))) - (jsonrpc-request - (eglot--current-server-or-lose) method (append - (eglot--TextDocumentPositionParams) - extra-params))))) + (let ((response + (jsonrpc-request + (eglot--current-server-or-lose) + method (append (eglot--TextDocumentPositionParams) extra-params)))) + (eglot--collecting-xrefs (collect) + (mapc + (eglot--lambda ((Location) uri range) + (collect (eglot--xref-make (symbol-at-point) uri range))) + (if (vectorp response) response (list response)))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) "Helper for `eglot-find-declaration' & friends." From 21c2bb18d898b5d70750688a51346b2b867e3265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Oct 2019 23:13:09 +0100 Subject: [PATCH 432/771] Protect against zero-length completions Apparently the Vue Language Server sends such things (see https://github.com/joaotavora/eglot/issues/319). * eglot.el (eglot-completion-at-point): Protect against zero-length completions. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aa64fe6a2a9..cba8c341b34 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1907,7 +1907,8 @@ is not active." (string-trim-left label)) (t (or insertText (string-trim-left label)))))) - (put-text-property 0 1 'eglot--lsp-item item proxy) + (unless (zerop (length proxy)) + (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) items)) (bounds From 5690e3a659b36717971e4f62108f82bda1b871d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 18 Oct 2019 16:43:50 +0100 Subject: [PATCH 433/771] Don't immediately request completions in eglot-completion-at-point Yet another adjustment to this function. According to the documentation of completion-at-point-functions, we should strive to make functions like eglot-completion-at-point "cheap to run". Requesting completion from the server immediately after calling the function goes against that. The reason we were doing it is that it might have helped compute more accurate "bounds" for the return value (START and END) from possible TextEdit completion items. But I've decided it's not worth the effort, at least for now. * eglot.el (eglot-completion-at-point): Request completions asynchronously. --- lisp/progmodes/eglot.el | 70 ++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cba8c341b34..09c1461a06b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1889,47 +1889,46 @@ is not active." (or (get-text-property 0 :sortText a) "") (or (get-text-property 0 :sortText b) "")))))) (metadata `(metadata . ((display-sort-function . ,sort-completions)))) - (response (jsonrpc-request server - :textDocument/completion - (eglot--CompletionParams) - :deferred :textDocument/completion - :cancel-on-input t)) - (items (append ; coerce to list - (if (vectorp response) response (plist-get response :items)) - nil)) + resp items (cached-proxies :none) (proxies - (mapcar (jsonrpc-lambda - (&rest item &key label insertText insertTextFormat - &allow-other-keys) - (let ((proxy - (cond ((and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)) - (string-trim-left label)) - (t - (or insertText (string-trim-left label)))))) - (unless (zerop (length proxy)) - (put-text-property 0 1 'eglot--lsp-item item proxy)) - proxy)) - items)) - (bounds - (cl-loop with probe = - (plist-get (plist-get (car items) :textEdit) :range) - for item in (cdr items) - for range = (plist-get (plist-get item :textEdit) :range) - unless (and range (equal range probe)) - return (bounds-of-thing-at-point 'symbol) - finally (cl-return (or (and probe - (eglot--range-region probe)) - (bounds-of-thing-at-point 'symbol)))))) + (lambda () + (if (listp cached-proxies) cached-proxies + (setq resp + (jsonrpc-request server + :textDocument/completion + (eglot--CompletionParams) + :deferred :textDocument/completion + :cancel-on-input t)) + (setq items (append + (if (vectorp resp) resp (plist-get resp :items)) + nil)) + (setq cached-proxies + (mapcar + (jsonrpc-lambda + (&rest item &key label insertText insertTextFormat + &allow-other-keys) + (let ((proxy + (cond ((and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)) + (string-trim-left label)) + (t + (or insertText (string-trim-left label)))))) + (unless (zerop (length item)) + (put-text-property 0 1 'eglot--lsp-item item proxy)) + proxy)) + items))))) + (bounds (bounds-of-thing-at-point 'symbol))) (list (or (car bounds) (point)) (or (cdr bounds) (point)) (lambda (probe pred action) (cond ((eq action 'metadata) metadata) ; metadata - ((eq action 'lambda) (member probe proxies)) ; test-completion + ((eq action 'lambda) ; test-completion + (member probe (funcall proxies))) ((eq (car-safe action) 'boundaries) nil) ; boundaries - ((and (null action) (member probe proxies) t)) ; try-completion + ((and (null action) ; try-completion + (member probe (funcall proxies)) t)) ((eq action t) ; all-completions (cl-remove-if-not (lambda (proxy) @@ -1938,7 +1937,7 @@ is not active." (and (or (null pred) (funcall pred proxy)) (string-prefix-p probe (or filterText proxy) completion-ignore-case)))) - proxies)))) + (funcall proxies))))) :annotation-function (lambda (proxy) (eglot--dbind ((CompletionItem) detail kind insertTextFormat) @@ -1993,7 +1992,8 @@ is not active." ;; buffer, `proxy' won't have any properties. A ;; lookup should fix that (github#148) (get-text-property - 0 'eglot--lsp-item (cl-find proxy proxies :test #'string=))) + 0 'eglot--lsp-item + (cl-find proxy (funcall proxies) :test #'string=))) (let ((snippet-fn (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)))) (cond (textEdit From 1002d7aeaf4306f2024e552a1ddca52961c2c56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 19 Oct 2019 23:07:47 +0100 Subject: [PATCH 434/771] Let user keep control of some variables during eglot sessions * NEWS.md: Mention new variable eglot-stay-out-of * eglot.el (eglot-stay-out-of): New variable. (eglot--setq-saving): Use it. (eglot--managed-mode): Use eglot--setq-saving for imenu. No need to remove 'eglot-flymake-backend from diagnostic functions. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/324 --- lisp/progmodes/eglot.el | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 09c1461a06b..7a22b5c33b3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1172,8 +1172,33 @@ and just return it. PROMPT shouldn't end with a question mark." (defvar-local eglot--saved-bindings nil "Bindings saved by `eglot--setq-saving'.") +(defvar eglot-stay-out-of '() + "List of Emacs things that Eglot should try to stay of. +Before Eglot starts \"managing\" a particular buffer, it +opinionatedly sets some peripheral Emacs facilites, such as +Flymake, Xref and Company. These overriding settings help ensure +consistent Eglot behaviour and only stay in place until +\"managing\" stops (usually via `eglot-shutdown'), whereupon the +previous settings are restored. + +However, if you wish for Eglot to stay out of a particular Emacs +facility that you'd like to keep control of, add a string, a +symbol, or a regexp here that will be matched against the +variable's name, and Eglot will refrain from setting it. + +For example, to keep your Company customization use + +(add-to-list 'eglot-stay-out-of 'company)") + (defmacro eglot--setq-saving (symbol binding) - `(when (boundp ',symbol) + `(when (and (boundp ',symbol) + (not (cl-find (symbol-name ',symbol) + eglot-stay-out-of + :test + (lambda (s thing) + (let ((re (if (symbolp thing) (symbol-name thing) + thing))) + (string-match re s)))))) (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) (setq-local ,symbol ,binding))) @@ -1200,11 +1225,10 @@ and just return it. PROMPT shouldn't end with a question mark." (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)) - (add-function :around (local 'imenu-create-index-function) #'eglot-imenu) + (eglot--setq-saving imenu-create-index-function #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) (t - (remove-hook 'flymake-diagnostic-functions 'eglot-flymake-backend t) (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) From 703a09113cf3e21a01c6c14b86faa97233c8f0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 20 Oct 2019 12:56:41 +0100 Subject: [PATCH 435/771] * eglot.el (version): bump to 1.5 * NEWS.md: update. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7a22b5c33b3..e78d2c45882 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.4 +;; Version: 1.5 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From f71716e91427d893773ca68d5d3dec417a963571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 21 Oct 2019 13:08:45 +0100 Subject: [PATCH 436/771] (again): fix issue with replace-buffer-contents Manually calling the before/after change hooks for Emacs 26.1's buggy replace-buffer-contents must be done with absolute positions, not markers. * eglot.el (eglot--apply-text-edits): Call change hooks with buffer positions, not markers. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/259 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e78d2c45882..e1c7b48c8d6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2307,7 +2307,9 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32237 ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32278 (let ((inhibit-modification-hooks t) - (length (- end beg))) + (length (- end beg)) + (beg (marker-position beg)) + (end (marker-position end))) (run-hook-with-args 'before-change-functions beg end) (replace-buffer-contents temp) From 0816da8e78f1d0d643fdd1259a2f307c0817e316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 21 Oct 2019 16:07:38 +0100 Subject: [PATCH 437/771] Unbreak m-x vc-revert, which reverts preserving modes Unlike the normal revert-buffer command, vc-revert, doesn't re-apply the major mode, meaning it was missing a didOpen to pair with the didClose that is unconditionally sent on both commands. Needed to use the dynamic variable revert-buffer-preserve-modes, and, curiously, also forward-declare it to appease the byte compiler. * eglot.el (eglot--managed-mode): Use after-revert-hook. (revert-buffer-preserve-modes): Forward declare. (eglot--after-revert-hook): Signal didOpen when preserving-modes. (eglot--maybe-activate-editing-mode): Tweak comment. --- lisp/progmodes/eglot.el | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e1c7b48c8d6..bd5f1ac8d4c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1214,6 +1214,7 @@ For example, to keep your Company customization use ;; Prepend "didClose" to the hook after the "onoff", so it will run first (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) + (add-hook 'after-revert-hook 'eglot--after-revert-hook nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) @@ -1233,6 +1234,7 @@ For example, to keep your Company customization use (remove-hook 'before-change-functions 'eglot--before-change t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) + (remove-hook 'after-revert-hook 'eglot--after-revert-hook t) (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) @@ -1286,6 +1288,11 @@ Reset in `eglot--managed-mode-onoff'.") (defvar-local eglot--unreported-diagnostics nil "Unreported Flymake diagnostics for this buffer.") +(defvar revert-buffer-preserve-modes) +(defun eglot--after-revert-hook () + "Eglot's `after-revert-hook'." + (when revert-buffer-preserve-modes (eglot--signal-textDocument/didOpen))) + (defun eglot--maybe-activate-editing-mode (&optional server) "Maybe activate mode function `eglot--managed-mode'. If SERVER is supplied, do it only if BUFFER is managed by it. In @@ -1297,7 +1304,8 @@ that case, also signal textDocument/didOpen." :eglot "`eglot--cached-current-server' is non-nil, but it shouldn't be!\n\ Please report this as a possible bug.") (setq eglot--cached-current-server nil))) - ;; Called even when revert-buffer-in-progress-p + ;; Called when `revert-buffer-in-progress-p' is t but + ;; `revert-buffer-preserve-modes' is nil. (let* ((cur (and buffer-file-name (eglot--current-server))) (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server From 72b9ef98652e00b47066dff4dca7934a89c16d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 21 Oct 2019 22:25:13 +0100 Subject: [PATCH 438/771] Fix race condition when company-completing quickly For some reason, probably related to the way that Eglot tries to maintain the responsiveness of Company completion tooltips (see below), the user's explicit input will sometimes be surprisingly deleted by Company, leading to a horrible completion experience. This is sometimes hard to reproduce, but appears to match this description perfectly: https://github.com/joaotavora/eglot/issues/319#issuecomment-542955432 Fortunately, Company has a good fix for this, which is to pass `:company-require-match 'never` in the completion-at-point function. This is the fix applied in this commit. However, this line shouldn't be required since the default value for `company-require-match` is `company-explicit-action-p`, presumably meaning that the auto-deletion should never take place for characters typed by the user. This points to a bug in Company, or at least something which may have been exacerbated by the way that Eglot aggressively fetches completions from the server by passing :cancel-on-input to `jsonrpc-request`, discarding out-of-date replies. Perhaps that discarding step bears with it some side-effects that make the `company-explicit-action-p` test return `nil` instead of the correct `t`. Company interprets this as carte blanche to delete the last inserted character. * eglot.el (eglot-completion-at-point): Use :company-require-match 'never. GitHub-reference: per https://github.com/joaotavora/eglot/issues/319 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bd5f1ac8d4c..92a35cab438 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2005,6 +2005,7 @@ is not active." (erase-buffer) (insert formatted) (current-buffer))))) + :company-require-match 'never :company-prefix-length (save-excursion (when (car bounds) (goto-char (car bounds))) From 3352f2b095c65e52962d53caa7c0f9f536063ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 22 Oct 2019 01:44:54 +0100 Subject: [PATCH 439/771] Force company to align completion annotations in eglot sessions * eglot.el (eglot--managed-mode): force company-tooltip-align-annotations to t. --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 92a35cab438..e29d8885774 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1226,6 +1226,7 @@ For example, to keep your Company customization use (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)) + (eglot--setq-saving company-tooltip-align-annotations t) (eglot--setq-saving imenu-create-index-function #'eglot-imenu) (flymake-mode 1) (eldoc-mode 1)) From 66f5a1a8ee247ee3f314f5c12f539ea75aeeb66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 22 Oct 2019 12:18:53 +0100 Subject: [PATCH 440/771] Unbreak imenu * eglot.el (eglot-imenu): Unbreak. --- lisp/progmodes/eglot.el | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e29d8885774..9a0960d7a96 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2252,10 +2252,11 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." :deferred :textDocument/documentHighlight)))) eldoc-last-message) -(defun eglot-imenu (oldfun) - "EGLOT's `imenu-create-index-function' overriding OLDFUN." - (if (eglot--server-capable :documentSymbolProvider) - (let ((entries +(defun eglot-imenu () + "EGLOT's `imenu-create-index-function'." + (unless (eglot--server-capable :documentSymbolProvider) + (eglot--error "Server isn't a :documentSymbolProvider")) + (let ((entries (mapcar (eglot--lambda ((SymbolInformation) name kind location containerName) @@ -2283,7 +2284,7 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." elems))))) (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) entries))) - (funcall oldfun))) + ) (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." From 615bd6ce378880ff3cb4bc7f2fc370495ab31017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 24 Oct 2019 12:32:51 +0100 Subject: [PATCH 441/771] Expand readme.md section on handling quirky servers Also remove explicit cquery support (cquery seems to be dead anyway). * README.md (Handling quirky servers): New section. * eglot.el (eglot-initialization-options eglot-cquery): Remove. --- lisp/progmodes/eglot.el | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9a0960d7a96..75ce9376104 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2511,19 +2511,6 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." "Handle notification window/progress" (setf (eglot--spinner server) (list id title done message))) - -;;; cquery-specific -;;; -(defclass eglot-cquery (eglot-lsp-server) () - :documentation "Cquery's C/C++ langserver.") - -(cl-defmethod eglot-initialization-options ((server eglot-cquery)) - "Passes through required cquery initialization options" - (let* ((root (car (project-roots (eglot--project server)))) - (cache (expand-file-name ".cquery_cached_index/" root))) - (list :cacheDirectory (file-name-as-directory cache) - :progressReportFrequencyMs -1))) - ;;; eclipse-jdt-specific ;;; From 4f6e4dc7a162ef2808de55cedc349843feda3d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 26 Oct 2019 22:51:57 +0100 Subject: [PATCH 442/771] Support workspace/configuration This helps users configure servers such as Gopls, which doesn't support didChangeConfiguration signals. * README.md (Per-project server configuration): New section. * eglot.el (eglot-workspace-configuration): Fix docstring. (eglot-signal-didChangeConfiguration): Rename a variable. (eglot-handle-request workspace/configuration): New request handler. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/326 --- lisp/progmodes/eglot.el | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 75ce9376104..b9496e166d2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -218,6 +218,7 @@ let the buffer grow forever." (defvar eglot--lsp-interface-alist `( (CodeAction (:title) (:kind :diagnostics :edit :command)) + (ConfigurationItem () (:scopeUri :section)) (Command (:title :command) (:arguments)) (CompletionItem (:label) (:kind :detail :documentation :deprecated :preselect @@ -474,7 +475,8 @@ treated as in `eglot-dbind'." :executeCommand `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges :json-false) :didChangeWatchedFiles `(:dynamicRegistration t) - :symbol `(:dynamicRegistration :json-false)) + :symbol `(:dynamicRegistration :json-false) + :configuration t) :textDocument (list :synchronization (list @@ -1655,9 +1657,9 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." '((name . eglot--signal-textDocument/didChange))) (defvar-local eglot-workspace-configuration () - "Alist of (SETTING . VALUE) entries configuring the LSP server. -Setting should be a keyword, value can be any value that can be -converted to JSON.") + "Alist of (SECTION . VALUE) entries configuring the LSP server. +SECTION should be a keyword or a string, value can be anything +that can be converted to JSON.") (put 'eglot-workspace-configuration 'safe-local-variable 'listp) @@ -1669,12 +1671,34 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (cl-loop for (k . v) in eglot-workspace-configuration - collect (if (keywordp k) - k - (intern (format ":%s" k))) + (cl-loop for (section . v) in eglot-workspace-configuration + collect (if (keywordp section) + section + (intern (format ":%s" section))) collect v)))) +(cl-defmethod eglot-handle-request + (server (_method (eql workspace/configuration)) &key items) + "Handle server request workspace/configuration." + (apply #'vector + (mapcar + (eglot--lambda ((ConfigurationItem) scopeUri section) + (let* ((path (eglot--uri-to-path scopeUri))) + (when (file-directory-p path) + (with-temp-buffer + (let ((default-directory path)) + (setq-local major-mode (eglot--major-mode server)) + (hack-dir-local-variables-non-file-buffer) + (alist-get section eglot-workspace-configuration + nil nil + (lambda (wsection section) + (string= + (if (keywordp wsection) + (substring (symbol-name wsection) 1) + wsection) + section)))))))) + items))) + (defun eglot--signal-textDocument/didChange () "Send textDocument/didChange to server." (when eglot--recent-changes From 7cda807726c633df17a08d53308d8a59220ec083 Mon Sep 17 00:00:00 2001 From: Xu Chunyang <4550353+xuchunyang@users.noreply.github.com> Date: Sun, 27 Oct 2019 23:41:53 +0800 Subject: [PATCH 443/771] Don't run mode hooks in eglot--format-markup * eglot.el (eglot--format-markup): Use delay-mode-hooks. Copyright-paperwork-exempt: yes --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b9496e166d2..285dc50ac84 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1083,7 +1083,9 @@ Doubles as an indicator of snippet support." major-mode)))) (with-temp-buffer (insert string) - (ignore-errors (funcall mode)) (font-lock-ensure) (buffer-string)))) + (ignore-errors (delay-mode-hooks (funcall mode))) + (font-lock-ensure) + (buffer-string)))) (defcustom eglot-ignored-server-capabilites (list) "LSP server capabilities that Eglot could use, but won't. From c8ea2c269a5452753875482a06f0de59b576dd96 Mon Sep 17 00:00:00 2001 From: Xu Chunyang <4550353+xuchunyang@users.noreply.github.com> Date: Mon, 28 Oct 2019 23:29:03 +0800 Subject: [PATCH 444/771] Support markdown for textdocument/hover () * eglot.el (eglot-client-capabilities): annouce markdown support for hover. (eglot--format-markup): Format hover info with Markdown. Fixes: https://github.com/joaotavora/eglot/issues/328 Copyright-paperwork-exempt: yes GitHub-reference: https://github.com/joaotavora/eglot/issues/329 --- lisp/progmodes/eglot.el | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 285dc50ac84..1f7a396ef98 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -489,7 +489,8 @@ treated as in `eglot-dbind'." t :json-false)) :contextSupport t) - :hover `(:dynamicRegistration :json-false) + :hover (list :dynamicRegistration :json-false + :contentFormat ["markdown" "plaintext"]) :signatureHelp (list :dynamicRegistration :json-false :signatureInformation `(:parameterInformation @@ -1080,7 +1081,9 @@ Doubles as an indicator of snippet support." (if (stringp markup) (list (string-trim markup) (intern "gfm-view-mode")) (list (plist-get markup :value) - major-mode)))) + (pcase (plist-get markup :kind) + ("markdown" 'gfm-view-mode) + (_ major-mode)))))) (with-temp-buffer (insert string) (ignore-errors (delay-mode-hooks (funcall mode))) From ee794a8d5ec095fabe0ce4b682b665cb97d3301d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 30 Oct 2019 01:24:10 +0000 Subject: [PATCH 445/771] Use completionitem/resolve more abundantly It was already used to resolve documentation bits of completions, but it can also be useful to resolve snippet templates and such. To resolve a completion, you need some part of a completion to start with. If it has a :data field exists and the server supports :resolveProvider, fetch the new object, otherwise use whatever we had already. * eglot.el (eglot-completion-at-point): Add another local function for resolving completions. GitHub-reference: per https://github.com/joaotavora/eglot/issues/50 --- lisp/progmodes/eglot.el | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1f7a396ef98..bce4ee08b15 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1979,6 +1979,20 @@ is not active." (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) items))))) + resolved + (resolve-maybe + ;; Maybe completion/resolve JSON object `lsp-comp' into + ;; another JSON object, if at all possible. Otherwise, + ;; just return lsp-comp. + (lambda (lsp-comp) + (cond (resolved resolved) + ((and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get lsp-comp :data)) + (setq resolved + (jsonrpc-request server :completionItem/resolve + lsp-comp :cancel-on-input t))) + (t lsp-comp)))) (bounds (bounds-of-thing-at-point 'symbol))) (list (or (car bounds) (point)) @@ -2021,13 +2035,7 @@ is not active." (lambda (proxy) (let* ((documentation (let ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy))) - (or (plist-get lsp-comp :documentation) - (and (eglot--server-capable :completionProvider - :resolveProvider) - (plist-get - (jsonrpc-request server :completionItem/resolve - lsp-comp :cancel-on-input t) - :documentation))))) + (plist-get (funcall resolve-maybe lsp-comp) :documentation))) (formatted (and documentation (eglot--format-markup documentation)))) (when formatted @@ -2050,13 +2058,15 @@ is not active." insertText textEdit additionalTextEdits) - (or (get-text-property 0 'eglot--lsp-item proxy) - ;; When selecting from the *Completions* - ;; buffer, `proxy' won't have any properties. A - ;; lookup should fix that (github#148) - (get-text-property - 0 'eglot--lsp-item - (cl-find proxy (funcall proxies) :test #'string=))) + (funcall + resolve-maybe + (or (get-text-property 0 'eglot--lsp-item proxy) + ;; When selecting from the *Completions* + ;; buffer, `proxy' won't have any properties. + ;; A lookup should fix that (github#148) + (get-text-property + 0 'eglot--lsp-item + (cl-find proxy (funcall proxies) :test #'string=)))) (let ((snippet-fn (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)))) (cond (textEdit From 27e0aa7333f84e2ea450309a801a5613fc9ede95 Mon Sep 17 00:00:00 2001 From: Ingo Lohmar Date: Fri, 18 Oct 2019 21:11:59 +0200 Subject: [PATCH 446/771] Merge -onoff proxy code into minor mode function This simplifies bookkeeping and keeping the state of locally cached servers, their managed buffers, and the buffer-local mode consistent. The "on" case of the -onoff code now expects that `eglot--cached-current-server' has been set already, the "off" case uses the same value. * eglot.el (eglot--managed-mode-onoff): Remove. (eglot--managed-mode): Adopt code. (eglot--managed-mode-off): New minimal wrapper. --- lisp/progmodes/eglot.el | 56 ++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bce4ee08b15..c4b83a02101 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -588,7 +588,7 @@ SERVER. ." (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) + (eglot--with-live-buffer buffer (eglot--managed-mode-off))) ;; Now ask jsonrpc.el to shut down the server (which under normal ;; conditions should return immediately). (jsonrpc-shutdown server (not preserve-buffers)) @@ -598,7 +598,7 @@ SERVER. ." "Called by jsonrpc.el when SERVER is already dead." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (eglot--with-live-buffer buffer (eglot--managed-mode-onoff server nil))) + (eglot--with-live-buffer buffer (eglot--managed-mode-off))) ;; Kill any expensive watches (maphash (lambda (_id watches) (mapcar #'file-notify-rm-watch watches)) @@ -1217,7 +1217,7 @@ For example, to keep your Company customization use (eglot--managed-mode (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) - (add-hook 'kill-buffer-hook 'eglot--managed-mode-onoff nil t) + (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t) ;; Prepend "didClose" to the hook after the "onoff", so it will run first (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) @@ -1226,7 +1226,7 @@ For example, to keep your Company customization use (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) - (add-hook 'change-major-mode-hook 'eglot--managed-mode-onoff nil t) + (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) @@ -1236,10 +1236,12 @@ For example, to keep your Company customization use (eglot--setq-saving company-tooltip-align-annotations t) (eglot--setq-saving imenu-create-index-function #'eglot-imenu) (flymake-mode 1) - (eldoc-mode 1)) + (eldoc-mode 1) + (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-current-server))) (t (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) + (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t) (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) (remove-hook 'after-revert-hook 'eglot--after-revert-hook t) @@ -1247,37 +1249,28 @@ For example, to keep your Company customization use (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) - (remove-hook 'change-major-mode-hook #'eglot--managed-mode-onoff t) + (remove-hook 'change-major-mode-hook #'eglot--managed-mode-off t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) - (setq eglot--current-flymake-report-fn nil)))) + (setq eglot--current-flymake-report-fn nil) + (let ((server eglot--cached-current-server)) + (setq eglot--cached-current-server nil) + (when server + (setf (eglot--managed-buffers server) + (delq (current-buffer) (eglot--managed-buffers server))) + (when (and eglot-autoshutdown + (not (eglot--shutdown-requested server)) + (not (eglot--managed-buffers server))) + (eglot-shutdown server))))))) + +(defun eglot--managed-mode-off () + "Turn off `eglot--managed-mode' unconditionally." + (eglot--managed-mode -1)) (defvar-local eglot--cached-current-server nil - "A cached reference to the current EGLOT server. -Reset in `eglot--managed-mode-onoff'.") - -(defun eglot--managed-mode-onoff (&optional server turn-on) - "Proxy for function `eglot--managed-mode' with TURN-ON and SERVER." - (let ((buf (current-buffer))) - (cond ((and server turn-on) - (eglot--managed-mode 1) - (setq eglot--cached-current-server server) - (cl-pushnew buf (eglot--managed-buffers server))) - (t - (eglot--managed-mode -1) - (let ((server - (or server - eglot--cached-current-server))) - (setq eglot--cached-current-server nil) - (when server - (setf (eglot--managed-buffers server) - (delq buf (eglot--managed-buffers server))) - (when (and eglot-autoshutdown - (not (eglot--shutdown-requested server)) - (not (eglot--managed-buffers server))) - (eglot-shutdown server)))))))) + "A cached reference to the current EGLOT server.") (defun eglot--current-server () "Find the current logical EGLOT server." @@ -1318,7 +1311,8 @@ Please report this as a possible bug.") (server (or (and (null server) cur) (and server (eq server cur) cur)))) (when server (setq eglot--unreported-diagnostics `(:just-opened . nil)) - (eglot--managed-mode-onoff server t) + (setq eglot--cached-current-server server) + (eglot--managed-mode) (eglot--signal-textDocument/didOpen))))) From 08532c1b92bcf43923273ef5f662705fe94e9126 Mon Sep 17 00:00:00 2001 From: Ingo Lohmar Date: Fri, 18 Oct 2019 21:19:46 +0200 Subject: [PATCH 447/771] Simplify "maybe"-activation, dump "server" arg * eglot.el (eglot--maybe-activate-editing-mode): Remove `server' arg. --- lisp/progmodes/eglot.el | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c4b83a02101..e304ebd2b28 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -864,7 +864,11 @@ This docstring appeases checkdoc, that's all." (jsonrpc-notify server :initialized (make-hash-table)) (dolist (buffer (buffer-list)) (with-current-buffer buffer - (eglot--maybe-activate-editing-mode server))) + ;; No need to pass SERVER as an argument: it has + ;; been registered in `eglot--servers-by-project', + ;; so that it can be obtained from the function + ;; `eglot--current-server' in any managed buffer. + (eglot--maybe-activate-editing-mode))) (setf (eglot--inhibit-autoreconnect server) (cond ((booleanp eglot-autoreconnect) @@ -1294,28 +1298,20 @@ For example, to keep your Company customization use "Eglot's `after-revert-hook'." (when revert-buffer-preserve-modes (eglot--signal-textDocument/didOpen))) -(defun eglot--maybe-activate-editing-mode (&optional server) - "Maybe activate mode function `eglot--managed-mode'. -If SERVER is supplied, do it only if BUFFER is managed by it. In -that case, also signal textDocument/didOpen." +(defun eglot--maybe-activate-editing-mode () + "Maybe activate `eglot--managed-mode'. + +If it is activated, also signal textDocument/didOpen." (unless eglot--managed-mode - (unless server - (when eglot--cached-current-server - (display-warning - :eglot "`eglot--cached-current-server' is non-nil, but it shouldn't be!\n\ -Please report this as a possible bug.") - (setq eglot--cached-current-server nil))) ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. - (let* ((cur (and buffer-file-name (eglot--current-server))) - (server (or (and (null server) cur) (and server (eq server cur) cur)))) + (let ((server (and buffer-file-name (eglot--current-server)))) (when server (setq eglot--unreported-diagnostics `(:just-opened . nil)) (setq eglot--cached-current-server server) (eglot--managed-mode) (eglot--signal-textDocument/didOpen))))) - (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) (add-hook 'after-change-major-mode-hook 'eglot--maybe-activate-editing-mode) From 19653f1e891d8a23afc46675579f9a3140fe582d Mon Sep 17 00:00:00 2001 From: Ingo Lohmar Date: Sun, 20 Oct 2019 12:21:37 +0200 Subject: [PATCH 448/771] Only set eglot--cached-current-server by (more aggressive) caching * eglot.el (eglot--current-server): Always set cache value. (eglot--maybe-activate-editing-mode): No need to set cached server. --- lisp/progmodes/eglot.el | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e304ebd2b28..a0f57f41602 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1277,13 +1277,15 @@ For example, to keep your Company customization use "A cached reference to the current EGLOT server.") (defun eglot--current-server () - "Find the current logical EGLOT server." + "Find and cache logical EGLOT server for current buffer." (or eglot--cached-current-server - (let* ((probe (or (project-current) - `(transient . ,default-directory)))) - (cl-find major-mode (gethash probe eglot--servers-by-project) - :key #'eglot--major-mode)))) + (setq eglot--cached-current-server + (cl-find major-mode + (gethash (or (project-current) + `(transient . ,default-directory)) + eglot--servers-by-project) + :key #'eglot--major-mode)))) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." @@ -1305,12 +1307,10 @@ If it is activated, also signal textDocument/didOpen." (unless eglot--managed-mode ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. - (let ((server (and buffer-file-name (eglot--current-server)))) - (when server - (setq eglot--unreported-diagnostics `(:just-opened . nil)) - (setq eglot--cached-current-server server) - (eglot--managed-mode) - (eglot--signal-textDocument/didOpen))))) + (when (and buffer-file-name (eglot--current-server)) + (setq eglot--unreported-diagnostics `(:just-opened . nil)) + (eglot--managed-mode) + (eglot--signal-textDocument/didOpen)))) (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) (add-hook 'after-change-major-mode-hook 'eglot--maybe-activate-editing-mode) From 8b4e81cdcf57a1eca09523c84bb3389d3549131d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 5 Nov 2019 23:53:35 +0000 Subject: [PATCH 449/771] Don't choke on workspace/configuration with no scopeuri * eglot.el (eglot-handle-request): Don't choke on nil scopeUri. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/340 --- lisp/progmodes/eglot.el | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a0f57f41602..7133426021b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1678,20 +1678,23 @@ When called interactively, use the currently active server" (apply #'vector (mapcar (eglot--lambda ((ConfigurationItem) scopeUri section) - (let* ((path (eglot--uri-to-path scopeUri))) - (when (file-directory-p path) - (with-temp-buffer - (let ((default-directory path)) - (setq-local major-mode (eglot--major-mode server)) - (hack-dir-local-variables-non-file-buffer) - (alist-get section eglot-workspace-configuration - nil nil - (lambda (wsection section) - (string= - (if (keywordp wsection) - (substring (symbol-name wsection) 1) - wsection) - section)))))))) + (with-temp-buffer + (let* ((uri-path (eglot--uri-to-path scopeUri)) + (default-directory + (if (and (not (string-empty-p uri-path)) + (file-directory-p uri-path)) + uri-path + (car (project-roots (eglot--project server)))))) + (setq-local major-mode (eglot--major-mode server)) + (hack-dir-local-variables-non-file-buffer) + (alist-get section eglot-workspace-configuration + nil nil + (lambda (wsection section) + (string= + (if (keywordp wsection) + (substring (symbol-name wsection) 1) + wsection) + section)))))) items))) (defun eglot--signal-textDocument/didChange () From a6799b92ce6208a12d17291673aa09e8963e61be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 9 Nov 2019 22:58:08 +0000 Subject: [PATCH 450/771] Provide stable eglot-current-server helper It's better if eglot--current-server is removed, since it was being abused by other packages, and has side effects. The only place where it was really needed was eglot--maybe-activate-editing-mode, so the find-and-cache logic has been moved there. All other places that can handle a nil server now use eglot-current-server, the external version. * eglot.el (eglot-shutdown, eglot, eglot--read-server) (eglot--mode-line-format): Use eglot-current-server. (eglot--connect): Update comment. (eglot--current-server): Remove. (eglot-current-server): New helper. (eglot--maybe-activate-editing-mode): find and cache the server here. * eglot-tests.el (auto-detect-running-server) (auto-shutdown, auto-reconnect, eglot-ensure) (slow-async-connection): Use eglot-current-server. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/342 --- lisp/progmodes/eglot.el | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7133426021b..281a293c899 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -576,7 +576,7 @@ If PRESERVE-BUFFERS is non-nil (interactively, when called with a prefix argument), do not kill events and output buffers of SERVER. ." (interactive (list (eglot--read-server "Shutdown which server" - (eglot--current-server)) + (eglot-current-server)) t nil current-prefix-arg)) (eglot--message "Asking %s politely to terminate" (jsonrpc-name server)) (unwind-protect @@ -726,7 +726,7 @@ described in `eglot-server-programs', which see. INTERACTIVE is t if called interactively." (interactive (append (eglot--guess-contact t) '(t))) - (let* ((current-server (eglot--current-server)) + (let* ((current-server (eglot-current-server)) (live-p (and current-server (jsonrpc-running-p current-server)))) (if (and live-p interactive @@ -866,8 +866,9 @@ This docstring appeases checkdoc, that's all." (with-current-buffer buffer ;; No need to pass SERVER as an argument: it has ;; been registered in `eglot--servers-by-project', - ;; so that it can be obtained from the function - ;; `eglot--current-server' in any managed buffer. + ;; so that it can be found (and cached) from + ;; `eglot--maybe-activate-editing-mode' in any + ;; managed buffer. (eglot--maybe-activate-editing-mode))) (setf (eglot--inhibit-autoreconnect server) (cond @@ -1159,7 +1160,7 @@ and just return it. PROMPT shouldn't end with a question mark." (cond ((null servers) (eglot--error "No servers!")) ((or (cdr servers) (not dont-if-just-the-one)) - (let* ((default (when-let ((current (eglot--current-server))) + (let* ((default (when-let ((current (eglot-current-server))) (funcall name current))) (read (completing-read (if default @@ -1276,20 +1277,13 @@ For example, to keep your Company customization use (defvar-local eglot--cached-current-server nil "A cached reference to the current EGLOT server.") -(defun eglot--current-server () - "Find and cache logical EGLOT server for current buffer." - (or - eglot--cached-current-server - (setq eglot--cached-current-server - (cl-find major-mode - (gethash (or (project-current) - `(transient . ,default-directory)) - eglot--servers-by-project) - :key #'eglot--major-mode)))) +(defun eglot-current-server () + "Return logical EGLOT server for current buffer, nil if none." + eglot--cached-current-server) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." - (or (eglot--current-server) + (or eglot--cached-current-server (jsonrpc-error "No current JSON-RPC connection"))) (defvar-local eglot--unreported-diagnostics nil @@ -1307,7 +1301,15 @@ If it is activated, also signal textDocument/didOpen." (unless eglot--managed-mode ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. - (when (and buffer-file-name (eglot--current-server)) + (when (and buffer-file-name + (or + eglot--cached-current-server + (setq eglot--cached-current-server + (cl-find major-mode + (gethash (or (project-current) + `(transient . ,default-directory)) + eglot--servers-by-project) + :key #'eglot--major-mode)))) (setq eglot--unreported-diagnostics `(:just-opened . nil)) (eglot--managed-mode) (eglot--signal-textDocument/didOpen)))) @@ -1354,7 +1356,7 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." - (pcase-let* ((server (eglot--current-server)) + (pcase-let* ((server (eglot-current-server)) (nick (and server (eglot--project-nickname server))) (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) From c4f5e40ddb1fe1e39d3b86875e04aedf46ec8bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 6 Nov 2019 15:11:12 +0000 Subject: [PATCH 451/771] Protect against empty-string inserttext in completions * eglot.el (eglot-completion-at-point): Don't use insertText as a proxy. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/341 --- lisp/progmodes/eglot.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 281a293c899..80c65cea5ce 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1968,8 +1968,11 @@ is not active." (cond ((and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)) (string-trim-left label)) + ((and insertText + (not (string-empty-p insertText))) + insertText) (t - (or insertText (string-trim-left label)))))) + (string-trim-left label))))) (unless (zerop (length item)) (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) From fbcb55168f1b0c718536fb35b68cad687d311636 Mon Sep 17 00:00:00 2001 From: r-zip Date: Tue, 12 Nov 2019 13:44:01 -0500 Subject: [PATCH 452/771] Set nobreak-char-display to nil in *eglot-help* * eglot.el (eglot-help-at-point): set nobreak-char-display Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/345 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 80c65cea5ce..0022737fb9d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2178,7 +2178,8 @@ is not active." (with-current-buffer (eglot--help-buffer) (with-help-window (current-buffer) (rename-buffer (format "*eglot-help for %s*" sym)) - (with-current-buffer standard-output (insert blurb))))))) + (with-current-buffer standard-output (insert blurb)) + (setq-local nobreak-char-display nil)))))) (defun eglot-doc-too-large-for-echo-area (string) "Return non-nil if STRING won't fit in echo area. From b5f02979b6c4a6f8b57279768258a5884e1fac5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Tue, 12 Nov 2019 13:50:11 +0100 Subject: [PATCH 453/771] Support serverinfo of lsp 3.15.0 Add support for serverInfo from the upcoming specification. This changeset just stores the info sent by the server and slightly changes a greeting message. But it opens up the possibility to identify servers even when eglot uses a TCP connection and therefore makes possible to implement server specific features (in eglot-x). Old message: ``` Connected! Server `EGLOT (test-ccls/c++-mode)' now managing `c++-mode' buffers in project `test-ccls'. ``` New message: ``` Connected! Server `ccls' now managing `c++-mode' buffers in project `test-ccls'. ``` * eglot.el (eglot--lsp-interface-alist): Extend it with serverInfo. (eglot-lsp-server): Add member variable server-info. (eglot--connect): Store server-info and display server's name when connected. --- lisp/progmodes/eglot.el | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0022737fb9d..a7b5d590045 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -229,7 +229,7 @@ let the buffer grow forever." (DocumentHighlight (:range) (:kind)) (FileSystemWatcher (:globPattern) (:kind)) (Hover (:contents) (:range)) - (InitializeResult (:capabilities)) + (InitializeResult (:capabilities) (:serverInfo)) (Location (:uri :range)) (LogMessageParams (:type :message)) (MarkupContent (:kind :value)) @@ -531,6 +531,9 @@ treated as in `eglot-dbind'." (capabilities :documentation "JSON object containing server capabilities." :accessor eglot--capabilities) + (server-info + :documentation "JSON object containing server info." + :accessor eglot--server-info) (shutdown-requested :documentation "Flag set when server is shutting down." :accessor eglot--shutdown-requested) @@ -856,11 +859,12 @@ This docstring appeases checkdoc, that's all." server) :capabilities (eglot-client-capabilities server)) :success-fn - (eglot--lambda ((InitializeResult) capabilities) + (eglot--lambda ((InitializeResult) capabilities serverInfo) (unless cancelled (push server (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) + (setf (eglot--server-info server) serverInfo) (jsonrpc-notify server :initialized (make-hash-table)) (dolist (buffer (buffer-list)) (with-current-buffer buffer @@ -888,7 +892,9 @@ This docstring appeases checkdoc, that's all." (eglot--message "Connected! Server `%s' now managing `%s' buffers \ in project `%s'." - (jsonrpc-name server) managed-major-mode + (or (plist-get serverInfo :name) + (jsonrpc-name server)) + managed-major-mode (eglot--project-nickname server)) (when tag (throw tag t)))) :timeout eglot-connect-timeout From 1f784797d86dd86625be3ed14226e795f031d2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 12 Nov 2019 08:51:09 +0000 Subject: [PATCH 454/771] Let other imenu functions work if lsp server's doesn't * eglot.el (eglot--stay-out-of-p): New helper. (eglot--setq-saving): Use it. (eglot--managed-mode): Use add-function :before-until for imenu-create-index-function. (eglot-imenu): Don't error. Fix indentation. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/343 --- lisp/progmodes/eglot.el | 81 ++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a7b5d590045..51d0dee140d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1208,17 +1208,16 @@ For example, to keep your Company customization use (add-to-list 'eglot-stay-out-of 'company)") +(defun eglot--stay-out-of-p (symbol) + "Tell if EGLOT should stay of of SYMBOL." + (cl-find (symbol-name symbol) eglot-stay-out-of + :test (lambda (s thing) + (let ((re (if (symbolp thing) (symbol-name thing) thing))) + (string-match re s))))) + (defmacro eglot--setq-saving (symbol binding) - `(when (and (boundp ',symbol) - (not (cl-find (symbol-name ',symbol) - eglot-stay-out-of - :test - (lambda (s thing) - (let ((re (if (symbolp thing) (symbol-name thing) - thing))) - (string-match re s)))))) - (push (cons ',symbol (symbol-value ',symbol)) - eglot--saved-bindings) + `(unless (or (not (boundp ',symbol)) (eglot--stay-out-of-p ',symbol)) + (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) (setq-local ,symbol ,binding))) (define-minor-mode eglot--managed-mode @@ -1245,7 +1244,8 @@ For example, to keep your Company customization use (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) - (eglot--setq-saving imenu-create-index-function #'eglot-imenu) + (unless (eglot--stay-out-of-p 'imenu) + (add-function :before-until imenu-create-index-function #'eglot-imenu)) (flymake-mode 1) (eldoc-mode 1) (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-current-server))) @@ -2298,37 +2298,36 @@ If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." (defun eglot-imenu () "EGLOT's `imenu-create-index-function'." - (unless (eglot--server-capable :documentSymbolProvider) - (eglot--error "Server isn't a :documentSymbolProvider")) (let ((entries - (mapcar - (eglot--lambda - ((SymbolInformation) name kind location containerName) - (cons (propertize - name - :kind (alist-get kind eglot--symbol-kind-names - "Unknown") - :containerName (and (stringp containerName) - (not (string-empty-p containerName)) - containerName)) - (eglot--lsp-position-to-point - (plist-get (plist-get location :range) :start)))) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/documentSymbol - `(:textDocument ,(eglot--TextDocumentIdentifier)))))) - (mapcar - (pcase-lambda (`(,kind . ,syms)) - (let ((syms-by-scope (seq-group-by - (lambda (e) - (get-text-property 0 :containerName (car e))) - syms))) - (cons kind (cl-loop for (scope . elems) in syms-by-scope - append (if scope - (list (cons scope elems)) - elems))))) - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries))) - ) + (and + (eglot--server-capable :documentSymbolProvider) + (mapcar + (eglot--lambda + ((SymbolInformation) name kind location containerName) + (cons (propertize + name + :kind (alist-get kind eglot--symbol-kind-names + "Unknown") + :containerName (and (stringp containerName) + (not (string-empty-p containerName)) + containerName)) + (eglot--lsp-position-to-point + (plist-get (plist-get location :range) :start)))) + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/documentSymbol + `(:textDocument ,(eglot--TextDocumentIdentifier))))))) + (mapcar + (pcase-lambda (`(,kind . ,syms)) + (let ((syms-by-scope (seq-group-by + (lambda (e) + (get-text-property 0 :containerName (car e))) + syms))) + (cons kind (cl-loop for (scope . elems) in syms-by-scope + append (if scope + (list (cons scope elems)) + elems))))) + (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) + entries)))) (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." From 9101970ccf77c755a12d20d147187cff20929df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 15 Nov 2019 16:04:55 +0000 Subject: [PATCH 455/771] Ensure process starts in project's root Also fix https://github.com/joaotavora/eglot/issues/347. * eglot.el (eglot--connect): Bind default-directory around make process. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/330 --- lisp/progmodes/eglot.el | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 51d0dee140d..b30a236ed19 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -813,15 +813,17 @@ This docstring appeases checkdoc, that's all." (setq autostart-inferior-process inferior) connection)))) ((stringp (car contact)) - `(:process ,(lambda () - (make-process - :name readable-name - :command contact - :connection-type 'pipe - :coding 'utf-8-emacs-unix - :noquery t - :stderr (get-buffer-create - (format "*%s stderr*" readable-name)))))))) + `(:process + ,(lambda () + (let ((default-directory default-directory)) + (make-process + :name readable-name + :command contact + :connection-type 'pipe + :coding 'utf-8-emacs-unix + :noquery t + :stderr (get-buffer-create + (format "*%s stderr*" readable-name))))))))) (spread (lambda (fn) (lambda (server method params) (apply fn server method (append params nil))))) (server @@ -1961,7 +1963,9 @@ is not active." :textDocument/completion (eglot--CompletionParams) :deferred :textDocument/completion - :cancel-on-input t)) + :cancel-on-input (prog1 non-essential + (when non-essential + (message "OH IT'S NON ESSENTIAL"))))) (setq items (append (if (vectorp resp) resp (plist-get resp :items)) nil)) From 751abfe9b4a8f882eca6a8903126641c2fb39489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 17 Nov 2019 13:07:08 +0000 Subject: [PATCH 456/771] * eglot.el (eglot-completion-at-point): remove spurious unrelated change. --- lisp/progmodes/eglot.el | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b30a236ed19..1baa3393dc6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1963,9 +1963,7 @@ is not active." :textDocument/completion (eglot--CompletionParams) :deferred :textDocument/completion - :cancel-on-input (prog1 non-essential - (when non-essential - (message "OH IT'S NON ESSENTIAL"))))) + :cancel-on-input t)) (setq items (append (if (vectorp resp) resp (plist-get resp :items)) nil)) From 46aa1aafd179353a98dc1582ca523bf649e3371c Mon Sep 17 00:00:00 2001 From: Xu Chunyang <4550353+xuchunyang@users.noreply.github.com> Date: Sun, 17 Nov 2019 21:17:47 +0800 Subject: [PATCH 457/771] Waste less space in completion annotations * eglot.el (eglot-completion-at-point): don't add "(snippet)" Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/349 --- lisp/progmodes/eglot.el | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1baa3393dc6..7171911b0e3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2033,10 +2033,7 @@ is not active." (when annotation (concat " " (propertize annotation - 'face 'font-lock-function-name-face) - (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn) - " (snippet)")))))) + 'face 'font-lock-function-name-face)))))) :company-doc-buffer (lambda (proxy) (let* ((documentation From 0453a2186634b5e9eca8e86748cdfe4002c3b855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Mon, 18 Nov 2019 12:23:37 +0100 Subject: [PATCH 458/771] Fail when eglot-find-* finds no references * eglot.el (eglot--lsp-xref-helper): Display message when no references have been found instead of calling xref-find-references. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/339 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7171911b0e3..c25b7b7f082 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1872,7 +1872,9 @@ Try to visit the target file for a richer summary line." method :extra-params extra-params :capability capability))) - (xref-find-references "LSP identifier at point."))) + (if eglot--lsp-xref-refs + (xref-find-references "LSP identifier at point.") + (eglot--message "%s returned no references" method)))) (defun eglot-find-declaration () "Find declaration for SYM, the identifier at point." From 111973220fd5822298beac47f18a8292b1e92d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 20 Nov 2019 22:51:42 +0000 Subject: [PATCH 459/771] Locally tweak imenu-create-index-function * eglot.el (eglot--managed-mode): locally tweak imenu-create-index-function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/351 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c25b7b7f082..a327655a0ea 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1247,7 +1247,8 @@ For example, to keep your Company customization use (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) (unless (eglot--stay-out-of-p 'imenu) - (add-function :before-until imenu-create-index-function #'eglot-imenu)) + (add-function :before-until (local 'imenu-create-index-function) + #'eglot-imenu)) (flymake-mode 1) (eldoc-mode 1) (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-current-server))) @@ -1267,6 +1268,7 @@ For example, to keep your Company customization use (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) + (remove-function (local 'imenu-create-index-function) #'eglot-imenu) (setq eglot--current-flymake-report-fn nil) (let ((server eglot--cached-current-server)) (setq eglot--cached-current-server nil) From 8f4b1d97dc7124d4768e3dae4047e52d8d9cfa71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 20 Nov 2019 22:55:29 +0000 Subject: [PATCH 460/771] Resolve compilation warnings * eglot.el (company-tooltip-align-annotations): Forward declare. (eglot--cached-server): Renamed from eglot--cached-current-server. (eglot--managed-mode, eglot-current-server) (eglot--current-server-or-lose) (eglot--maybe-activate-editing-mode): use it. (eglot-completion-at-point): Don't use insertTextFormat. --- lisp/progmodes/eglot.el | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a327655a0ea..90f2c684745 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -71,7 +71,8 @@ (require 'filenotify) (require 'ert) (require 'array) -(defvar company-backends) ; forward-declare, but don't require company yet +(defvar company-backends) ; forward-declare, but don't require company +(defvar company-tooltip-align-annotations) @@ -1222,6 +1223,9 @@ For example, to keep your Company customization use (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) (setq-local ,symbol ,binding))) +(defvar-local eglot--cached-server nil + "A cached reference to the current EGLOT server.") + (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map @@ -1251,7 +1255,7 @@ For example, to keep your Company customization use #'eglot-imenu)) (flymake-mode 1) (eldoc-mode 1) - (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-current-server))) + (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-server))) (t (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -1270,8 +1274,8 @@ For example, to keep your Company customization use do (set (make-local-variable var) saved-binding)) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) (setq eglot--current-flymake-report-fn nil) - (let ((server eglot--cached-current-server)) - (setq eglot--cached-current-server nil) + (let ((server eglot--cached-server)) + (setq eglot--cached-server nil) (when server (setf (eglot--managed-buffers server) (delq (current-buffer) (eglot--managed-buffers server))) @@ -1284,16 +1288,13 @@ For example, to keep your Company customization use "Turn off `eglot--managed-mode' unconditionally." (eglot--managed-mode -1)) -(defvar-local eglot--cached-current-server nil - "A cached reference to the current EGLOT server.") - (defun eglot-current-server () "Return logical EGLOT server for current buffer, nil if none." - eglot--cached-current-server) + eglot--cached-server) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." - (or eglot--cached-current-server + (or eglot--cached-server (jsonrpc-error "No current JSON-RPC connection"))) (defvar-local eglot--unreported-diagnostics nil @@ -1313,8 +1314,8 @@ If it is activated, also signal textDocument/didOpen." ;; `revert-buffer-preserve-modes' is nil. (when (and buffer-file-name (or - eglot--cached-current-server - (setq eglot--cached-current-server + eglot--cached-server + (setq eglot--cached-server (cl-find major-mode (gethash (or (project-current) `(transient . ,default-directory)) @@ -2026,7 +2027,7 @@ is not active." (funcall proxies))))) :annotation-function (lambda (proxy) - (eglot--dbind ((CompletionItem) detail kind insertTextFormat) + (eglot--dbind ((CompletionItem) detail kind) (get-text-property 0 'eglot--lsp-item proxy) (let* ((detail (and (stringp detail) (not (string= detail "")) From 86da1f615c0d1087b504c4b52347ac1b765834a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 29 Nov 2019 23:33:12 +0000 Subject: [PATCH 461/771] Unbreak window/showmessagerequest * eglot.el (eglot-handle-request): Answer with a proper MessageActionItem. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/362 --- lisp/progmodes/eglot.el | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 90f2c684745..b3a7f518267 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1450,16 +1450,16 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-request (_server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" - (or (completing-read - (concat - (format (propertize "[eglot] Server reports (type=%s): %s" - 'face (if (<= type 1) 'error)) - type message) - "\nChoose an option: ") - (or (mapcar (lambda (obj) (plist-get obj :title)) actions) - '("OK")) - nil t (plist-get (elt actions 0) :title)) - (jsonrpc-error :code -32800 :message "User cancelled"))) + (let ((label (completing-read + (concat + (format (propertize "[eglot] Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message) + "\nChoose an option: ") + (or (mapcar (lambda (obj) (plist-get obj :title)) actions) + '("OK")) + nil t (plist-get (elt actions 0) :title)))) + (if label `(:title ,label) :null))) (cl-defmethod eglot-handle-notification (_server (_method (eql window/logMessage)) &key _type _message) From 084970d188d0adf6c96f93c852caee6088432416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 30 Nov 2019 00:16:12 +0000 Subject: [PATCH 462/771] Allow non-standard keys in textdocument/publishdiagnostics. * eglot.el (eglot-handle-notification): Allow other keys for textDocument/publishDiagnostics. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/357 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b3a7f518267..74f06699f23 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1470,7 +1470,8 @@ COMMAND is a symbol naming the command." "Handle notification telemetry/event") ;; noop, use events buffer (cl-defmethod eglot-handle-notification - (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics) + (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics + &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' "Handle notification publishDiagnostics" (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer From 5c235178991116612a8fa51babe332a4e2fa9553 Mon Sep 17 00:00:00 2001 From: Antoine Kalmbach Date: Tue, 17 Dec 2019 21:34:29 +0200 Subject: [PATCH 463/771] Add metals as the language server for scala * README.md (Connecting to a server): Add metals to the list * eglot.el (eglot-server-programs): Add metals for scala-mode Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/376 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 74f06699f23..3e8eb2d14fd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -104,7 +104,8 @@ language-server/bin/php-language-server.php")) (java-mode . eglot--eclipse-jdt-contact) (dart-mode . ("dart_language_server")) (elixir-mode . ("language_server.sh")) - (ada-mode . ("ada_language_server"))) + (ada-mode . ("ada_language_server")) + (scala-mode . ("metals-emacs"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From 76a658aba15103a60cc427463b677429b0a67eb5 Mon Sep 17 00:00:00 2001 From: Augusto Stoffel Date: Wed, 18 Dec 2019 13:44:28 +0100 Subject: [PATCH 464/771] Add built-in support for tex and friends plain-tex-mode and latex-mode are derived from tex-mode. Some other TeX-related modes are not, so they require an explicit mention in eglot-server-programs. * README.md (Connecting to a server): Add Digestif to the list * eglot.el (eglot-server-programs): Add Digestif for TeX-related modes Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/379 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3e8eb2d14fd..4c0160a5705 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -105,7 +105,9 @@ language-server/bin/php-language-server.php")) (dart-mode . ("dart_language_server")) (elixir-mode . ("language_server.sh")) (ada-mode . ("ada_language_server")) - (scala-mode . ("metals-emacs"))) + (scala-mode . ("metals-emacs")) + ((tex-mode context-mode texinfo-mode bibtex-mode) + . ("digestif"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From be17d1824fe1f6cfd863c2ac7ddad224f7d251cb Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Thu, 26 Dec 2019 09:39:33 +0100 Subject: [PATCH 465/771] New eglot-confirm-server-initiated-edits defcustom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot-confirm-server-initiated-edits): New defcustom. Copyright-paperwork-exempt: yes Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/382 --- lisp/progmodes/eglot.el | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4c0160a5705..df8df13410a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -193,6 +193,11 @@ let the buffer grow forever." :type '(choice (const :tag "No limit" nil) (integer :tag "Number of characters"))) +(defcustom eglot-confirm-server-initiated-edits 'confirm + "Non-nil if server-initiated edits should be confirmed with user." + :type '(choice (const :tag "Don't show confirmation prompt" nil) + (symbol :tag "Show confirmation prompt" 'confirm))) + ;;; Constants ;;; @@ -1547,7 +1552,7 @@ THINGS are either registrations or unregisterations (sic)." (cl-defmethod eglot-handle-request (_server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit" - (eglot--apply-workspace-edit edit 'confirm)) + (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." From 1668a22f0208223f103616dc93a7963ff870e0d8 Mon Sep 17 00:00:00 2001 From: Steve Purcell Date: Sun, 29 Dec 2019 02:08:19 +1300 Subject: [PATCH 466/771] Add elm-language-server as the language server for elm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * README.md (Connecting to a server): Add elm-language-server * eglot.el (eglot-server-programs): Add elm-language-server Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/383 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index df8df13410a..5abdfc54af0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -97,6 +97,7 @@ language-server/bin/php-language-server.php")) . ("solargraph" "socket" "--port" :autoport)) (haskell-mode . ("hie-wrapper")) + (elm-mode . ("elm-language-server")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" From d8a8bf448aed3a58f31f3a1556b11e8d7de8ea41 Mon Sep 17 00:00:00 2001 From: Evgeni Kolev Date: Mon, 30 Dec 2019 11:13:08 +0200 Subject: [PATCH 467/771] * eglot.el (eglot-eldoc-function): fix outdated docstring. Fix https://github.com/joaotavora/eglot/issues/387 --- lisp/progmodes/eglot.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5abdfc54af0..0d2433e9c05 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2254,8 +2254,7 @@ potentially rename EGLOT's help buffer." (eldoc-message string))) (defun eglot-eldoc-function () - "EGLOT's `eldoc-documentation-function' function. -If SKIP-SIGNATURE, don't try to send textDocument/signatureHelp." + "EGLOT's `eldoc-documentation-function' function." (let* ((buffer (current-buffer)) (server (eglot--current-server-or-lose)) (position-params (eglot--TextDocumentPositionParams)) From 234bbd10321cb668df253323a9033cf692642c0c Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Thu, 2 Jan 2020 10:33:26 +0100 Subject: [PATCH 468/771] Use completing-read in eglot-code-actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See also https://github.com/joaotavora/eglot/issues/386. * eglot.el (eglot-code-actions): Replace tmm with completing-read Copyright-paperwork-exempt: yes Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/393 --- lisp/progmodes/eglot.el | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0d2433e9c05..d1ff7da5d9c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2466,12 +2466,10 @@ potentially rename EGLOT's help buffer." (menu `("Eglot code actions:" ("dummy" ,@menu-items))) (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) - (let ((never-mind (gensym)) retval) - (setcdr (cadr menu) - (cons `("never mind..." . ,never-mind) (cdadr menu))) - (if (eq (setq retval (tmm-prompt menu)) never-mind) - (keyboard-quit) - retval))))) + (cdr (assoc (completing-read "[eglot] Pick an action: " + menu-items nil t + nil nil (car menu-items)) + menu-items))))) (eglot--dcase action (((Command) command arguments) (eglot-execute-command server (intern command) arguments)) From edbc24d9cd6e05291531a65a78bb960439ceaae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 1 Jan 2020 22:05:29 +0000 Subject: [PATCH 469/771] Avoid double shutdowns and simplify shutdown logic * eglot.el (eglot-shutdown): Don't turn off eglot--managed-mode here. (eglot--on-shutdown): Rather here, but without autoshutdown. (eglot--managed-mode): Don't check eglot--shutdown-requested. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/389 --- lisp/progmodes/eglot.el | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d1ff7da5d9c..e952b912d16 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -599,9 +599,6 @@ SERVER. ." ;; this one is supposed to always fail, because it asks the ;; server to exit itself. Hence ignore-errors. (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) - ;; Turn off `eglot--managed-mode' where appropriate. - (dolist (buffer (eglot--managed-buffers server)) - (eglot--with-live-buffer buffer (eglot--managed-mode-off))) ;; Now ask jsonrpc.el to shut down the server (which under normal ;; conditions should return immediately). (jsonrpc-shutdown server (not preserve-buffers)) @@ -611,7 +608,9 @@ SERVER. ." "Called by jsonrpc.el when SERVER is already dead." ;; Turn off `eglot--managed-mode' where appropriate. (dolist (buffer (eglot--managed-buffers server)) - (eglot--with-live-buffer buffer (eglot--managed-mode-off))) + (let (;; Avoid duplicate shutdowns (github#389) + (eglot-autoshutdown nil)) + (eglot--with-live-buffer buffer (eglot--managed-mode-off)))) ;; Kill any expensive watches (maphash (lambda (_id watches) (mapcar #'file-notify-rm-watch watches)) @@ -1289,8 +1288,7 @@ For example, to keep your Company customization use (setf (eglot--managed-buffers server) (delq (current-buffer) (eglot--managed-buffers server))) (when (and eglot-autoshutdown - (not (eglot--shutdown-requested server)) - (not (eglot--managed-buffers server))) + (null (eglot--managed-buffers server))) (eglot-shutdown server))))))) (defun eglot--managed-mode-off () From 03ac6a10acc4fa677f9dbff468a72ff4fbf06052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Sat, 4 Jan 2020 16:55:00 +0100 Subject: [PATCH 470/771] Send exit as a notification This is what the specification requires. @PerMildner, thanks for reporting and analyzing the issue. * eglot.el (eglot-shutdown): Use `notify' instead of `request' for the `exit' LSP method. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/277 --- lisp/progmodes/eglot.el | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e952b912d16..888eacc690a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -596,11 +596,8 @@ SERVER. ." (progn (setf (eglot--shutdown-requested server) t) (jsonrpc-request server :shutdown nil :timeout (or timeout 1.5)) - ;; this one is supposed to always fail, because it asks the - ;; server to exit itself. Hence ignore-errors. - (ignore-errors (jsonrpc-request server :exit nil :timeout 1))) - ;; Now ask jsonrpc.el to shut down the server (which under normal - ;; conditions should return immediately). + (jsonrpc-notify server :exit nil)) + ;; Now ask jsonrpc.el to shut down the server. (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) From f9b59cf71f9aa31296642231856536be1618035f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 29 Nov 2019 23:42:58 +0000 Subject: [PATCH 471/771] Abide by lsp when reporting and moving to columns * eglot.el (eglot-current-column-function): Set to eglot-lsp-abiding-column. (eglot-move-to-column-function): Set to eglot-move-to-lsp-abiding-column. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/361 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 888eacc690a..cbfb926036a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -999,7 +999,7 @@ CONNECT-ARGS are passed as additional arguments to (defun eglot-current-column () (- (point) (point-at-bol))) -(defvar eglot-current-column-function #'eglot-current-column +(defvar eglot-current-column-function #'eglot-lsp-abiding-column "Function to calculate the current column. This is the inverse operation of @@ -1023,7 +1023,7 @@ for all others.") :character (progn (when pos (goto-char pos)) (funcall eglot-current-column-function))))) -(defvar eglot-move-to-column-function #'eglot-move-to-column +(defvar eglot-move-to-column-function #'eglot-move-to-lsp-abiding-column "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based From 6e0ad2ac68820ef197116ea4149ee60f40797a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Wed, 8 Jan 2020 16:51:09 +0100 Subject: [PATCH 472/771] Document the changes in column calculation * eglot.el (eglot-current-column-function) (eglot-move-to-column-function): Document the change of the default value. * NEWS.md: Log the change here as well. --- lisp/progmodes/eglot.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cbfb926036a..5e83118554f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1006,8 +1006,8 @@ This is the inverse operation of `eglot-move-to-column-function' (which see). It is a function of no arguments returning a column number. For buffers managed by fully LSP-compliant servers, this should be set to -`eglot-lsp-abiding-column', and `eglot-current-column' (the default) -for all others.") +`eglot-lsp-abiding-column' (the default), and +`eglot-current-column' for all others.") (defun eglot-lsp-abiding-column () "Calculate current COLUMN as defined by the LSP spec." @@ -1033,8 +1033,8 @@ where X is a multi-byte character, it actually means `b', not `c'. However, many servers don't follow the spec this closely. For buffers managed by fully LSP-compliant servers, this should -be set to `eglot-move-to-lsp-abiding-column', and -`eglot-move-to-column' (the default) for all others.") +be set to `eglot-move-to-lsp-abiding-column' (the default), and +`eglot-move-to-column' for all others.") (defun eglot-move-to-column (column) "Move to COLUMN without closely following the LSP spec." From 4ff8f1ed8f36dc8987819424f02133da526899b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Wed, 8 Jan 2020 18:02:07 +0100 Subject: [PATCH 473/771] Revert the last change about column calculation --- lisp/progmodes/eglot.el | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5e83118554f..888eacc690a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -999,15 +999,15 @@ CONNECT-ARGS are passed as additional arguments to (defun eglot-current-column () (- (point) (point-at-bol))) -(defvar eglot-current-column-function #'eglot-lsp-abiding-column +(defvar eglot-current-column-function #'eglot-current-column "Function to calculate the current column. This is the inverse operation of `eglot-move-to-column-function' (which see). It is a function of no arguments returning a column number. For buffers managed by fully LSP-compliant servers, this should be set to -`eglot-lsp-abiding-column' (the default), and -`eglot-current-column' for all others.") +`eglot-lsp-abiding-column', and `eglot-current-column' (the default) +for all others.") (defun eglot-lsp-abiding-column () "Calculate current COLUMN as defined by the LSP spec." @@ -1023,7 +1023,7 @@ fully LSP-compliant servers, this should be set to :character (progn (when pos (goto-char pos)) (funcall eglot-current-column-function))))) -(defvar eglot-move-to-column-function #'eglot-move-to-lsp-abiding-column +(defvar eglot-move-to-column-function #'eglot-move-to-column "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based @@ -1033,8 +1033,8 @@ where X is a multi-byte character, it actually means `b', not `c'. However, many servers don't follow the spec this closely. For buffers managed by fully LSP-compliant servers, this should -be set to `eglot-move-to-lsp-abiding-column' (the default), and -`eglot-move-to-column' for all others.") +be set to `eglot-move-to-lsp-abiding-column', and +`eglot-move-to-column' (the default) for all others.") (defun eglot-move-to-column (column) "Move to COLUMN without closely following the LSP spec." From edf382a98222cf8a133bdc119563aba22a849bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Thu, 9 Jan 2020 13:28:08 -0500 Subject: [PATCH 474/771] Support bug-reference-prog-mode * eglot.el (Local Variables): Add bug-reference-bug-regexp and bug-reference-url-format. GitHub-reference: close https://github.com/joaotavora/eglot/issues/405 --- lisp/progmodes/eglot.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 888eacc690a..1decb2695fc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2661,5 +2661,7 @@ If INTERACTIVE, prompt user for details." ;;; eglot.el ends here ;; Local Variables: +;; bug-reference-bug-regexp: "\\(github#\\([0-9]+\\)\\)" +;; bug-reference-url-format: "https://github.com/joaotavora/eglot/issues/%s" ;; checkdoc-force-docstrings-flag: nil ;; End: From 70e6157b56a6d1534148f6fd9706232754e9ea9e Mon Sep 17 00:00:00 2001 From: Vladimir Panteleev Date: Sun, 6 Oct 2019 16:10:32 +0000 Subject: [PATCH 475/771] Call shutdown/exit methods with params:{}, not null "null" is not a valid JSON value for "params" according to the JSON-RPC specification. * eglot.el (eglot-shutdown): Do the same thing as for "initialized", and use an empty hash table to be serialized to {}. Copyright-paperwork-exempt: yes GitHub-reference: per https://github.com/joaotavora/eglot/issues/315 --- lisp/progmodes/eglot.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1decb2695fc..1bdca12642d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -595,8 +595,9 @@ SERVER. ." (unwind-protect (progn (setf (eglot--shutdown-requested server) t) - (jsonrpc-request server :shutdown nil :timeout (or timeout 1.5)) - (jsonrpc-notify server :exit nil)) + (jsonrpc-request server :shutdown (make-hash-table) + :timeout (or timeout 1.5)) + (jsonrpc-notify server :exit (make-hash-table))) ;; Now ask jsonrpc.el to shut down the server. (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) From f6a72c5541f3e1564476109a06f3899f5a663882 Mon Sep 17 00:00:00 2001 From: Vladimir Panteleev Date: Tue, 19 Nov 2019 02:04:43 +0000 Subject: [PATCH 476/771] Introduce and use eglot--{}, the empty json object * eglot.el (Constants): Add eglot--{}. (eglot-shutdown, eglot--connect): Use it. Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/315 --- lisp/progmodes/eglot.el | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1bdca12642d..7f506c8c9ff 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -220,6 +220,8 @@ let the buffer grow forever." (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") (17 . "File") (18 . "Reference"))) +(defconst eglot--{} (make-hash-table) "The empty JSON object.") + ;;; Message verification helpers @@ -595,9 +597,9 @@ SERVER. ." (unwind-protect (progn (setf (eglot--shutdown-requested server) t) - (jsonrpc-request server :shutdown (make-hash-table) + (jsonrpc-request server :shutdown eglot--{} :timeout (or timeout 1.5)) - (jsonrpc-notify server :exit (make-hash-table))) + (jsonrpc-notify server :exit eglot--{})) ;; Now ask jsonrpc.el to shut down the server. (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) @@ -874,7 +876,7 @@ This docstring appeases checkdoc, that's all." (gethash project eglot--servers-by-project)) (setf (eglot--capabilities server) capabilities) (setf (eglot--server-info server) serverInfo) - (jsonrpc-notify server :initialized (make-hash-table)) + (jsonrpc-notify server :initialized eglot--{}) (dolist (buffer (buffer-list)) (with-current-buffer buffer ;; No need to pass SERVER as an argument: it has From fbc29353667b88c208b2796a4e1e120edb708673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Fri, 22 Nov 2019 16:55:04 +0100 Subject: [PATCH 477/771] Add public hook eglot-managed-mode-hook Per https://github.com/joaotavora/eglot/issues/354. * eglot.el (eglot-managed-p): New function. (eglot--managed-mode-hook): Obsolete it. (eglot-managed-mode-hook): New hook variable. (eglot--managed-mode): Run the new hook. * README.md (Customization): Mention the new hook. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/182 --- lisp/progmodes/eglot.el | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7f506c8c9ff..4c65af0a24c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1234,6 +1234,17 @@ For example, to keep your Company customization use (defvar-local eglot--cached-server nil "A cached reference to the current EGLOT server.") +(defun eglot-managed-p () + "Tell if current buffer is managed by EGLOT." + eglot--managed-mode) + +(make-obsolete-variable + 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") + +(defvar eglot-managed-mode-hook nil + "A hook run by EGLOT after it started/stopped managing a buffer. +Use `eglot-managed-p' to determine if current buffer is managed.") + (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." nil nil eglot-mode-map @@ -1289,7 +1300,9 @@ For example, to keep your Company customization use (delq (current-buffer) (eglot--managed-buffers server))) (when (and eglot-autoshutdown (null (eglot--managed-buffers server))) - (eglot-shutdown server))))))) + (eglot-shutdown server)))))) + ;; Note: the public hook runs before the internal eglot--managed-mode-hook. + (run-hooks 'eglot-managed-mode-hook)) (defun eglot--managed-mode-off () "Turn off `eglot--managed-mode' unconditionally." From e81e6a24adf9eaaab0e31c4056ae7bef7a94db72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Fri, 22 Nov 2019 18:35:01 +0100 Subject: [PATCH 478/771] Make a public reader for project-nickname Close https://github.com/joaotavora/eglot/issues/399. * eglot.el (eglot-lsp-server): Add a public reader for project-nickname as eglot-project-nickname. (eglot--connect, eglot--read-server, eglot--mode-line-format): Use the public variant. GitHub-reference: per https://github.com/joaotavora/eglot/issues/354 --- lisp/progmodes/eglot.el | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4c65af0a24c..8d1d2d71574 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -536,7 +536,8 @@ treated as in `eglot-dbind'." (defclass eglot-lsp-server (jsonrpc-process-connection) ((project-nickname :documentation "Short nickname for the associated project." - :accessor eglot--project-nickname) + :accessor eglot--project-nickname + :reader eglot-project-nickname) (major-mode :documentation "Major mode symbol." :accessor eglot--major-mode) @@ -906,7 +907,7 @@ in project `%s'." (or (plist-get serverInfo :name) (jsonrpc-name server)) managed-major-mode - (eglot--project-nickname server)) + (eglot-project-nickname server)) (when tag (throw tag t)))) :timeout eglot-connect-timeout :error-fn (eglot--lambda ((ResponseError) code message) @@ -1172,7 +1173,7 @@ and just return it. PROMPT shouldn't end with a question mark." being hash-values of eglot--servers-by-project append servers)) (name (lambda (srv) - (format "%s/%s" (eglot--project-nickname srv) + (format "%s/%s" (eglot-project-nickname srv) (eglot--major-mode srv))))) (cond ((null servers) (eglot--error "No servers!")) @@ -1388,7 +1389,7 @@ Uses THING, FACE, DEFS and PREPEND." (defun eglot--mode-line-format () "Compose the EGLOT's mode-line." (pcase-let* ((server (eglot-current-server)) - (nick (and server (eglot--project-nickname server))) + (nick (and server (eglot-project-nickname server))) (pending (and server (hash-table-count (jsonrpc--request-continuations server)))) (`(,_id ,doing ,done-p ,_detail) (and server (eglot--spinner server))) From 595ca62d1c7aec15e364011c99ff986ea2e930ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Fri, 20 Mar 2020 09:42:44 +0100 Subject: [PATCH 479/771] Ignore empty hover info This just mimics a similar check in `eglot-help-at-point'. * eglot.el (eglot-eldoc-function): Check emptiness of `contents' more carefully. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/425 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8d1d2d71574..fe06e0c9ada 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2296,7 +2296,7 @@ potentially rename EGLOT's help buffer." :success-fn (eglot--lambda ((Hover) contents range) (unless sig-showing (when-buffer-window - (when-let (info (and contents + (when-let (info (and (not (seq-empty-p contents)) (eglot--hover-info contents range))) (eglot--update-doc info thing-at-point))))) From 6b59dcf652301a98d96e539509fad26a873db6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Thu, 16 Apr 2020 09:38:31 +0200 Subject: [PATCH 480/771] Send shutdown and exit messages without arguments Fix regression introduced in 70e6157b (https://github.com/joaotavora/eglot/issues/315). According to the LSP specification the exit notification and the shutdown request shouldn't have arguments ("params: void"). Note that jsonrpc.el send nil as null on the wire. * eglot.el (eglot-shutdown): Change back the arguments of :shutdown and :exit to nil. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/430 --- lisp/progmodes/eglot.el | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fe06e0c9ada..3149cd5cc01 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -598,9 +598,8 @@ SERVER. ." (unwind-protect (progn (setf (eglot--shutdown-requested server) t) - (jsonrpc-request server :shutdown eglot--{} - :timeout (or timeout 1.5)) - (jsonrpc-notify server :exit eglot--{})) + (jsonrpc-request server :shutdown nil :timeout (or timeout 1.5)) + (jsonrpc-notify server :exit nil)) ;; Now ask jsonrpc.el to shut down the server. (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) From c93c90842a96f8695ec81f1e34ff4ca951737f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 19 Jan 2020 11:02:55 +0100 Subject: [PATCH 481/771] Fix eglot-move-to-lsp-abiding-column () Ensure conformance with the this part of the specification: "if the character value is greater than the line length it defaults back to the line length." * eglot.el: (eglot-move-to-lsp-abiding-column): Don't move beyond line-end. GitHub-reference: https://github.com/joaotavora/eglot/issues/361 --- lisp/progmodes/eglot.el | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3149cd5cc01..2a50611d364 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1050,15 +1050,20 @@ be set to `eglot-move-to-lsp-abiding-column', and (defun eglot-move-to-lsp-abiding-column (column) "Move to COLUMN abiding by the LSP spec." - (cl-loop - initially (move-to-column column) - with lbp = (line-beginning-position) - for diff = (- column - (/ (- (length (encode-coding-region lbp (point) 'utf-16 t)) - 2) - 2)) - until (zerop diff) - do (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)))) + (save-restriction + (cl-loop + with lbp = (line-beginning-position) + initially + (narrow-to-region lbp (line-end-position)) + (move-to-column column) + for diff = (- column + (/ (- (length (encode-coding-region lbp (point) 'utf-16 t)) + 2) + 2)) + until (zerop diff) + do (condition-case eob-err + (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) + (end-of-buffer (cl-return eob-err)))))) (defun eglot--lsp-position-to-point (pos-plist &optional marker) "Convert LSP position POS-PLIST to Emacs point. From f901fa91bc61454363a07989e458a3a05bca080f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Sun, 19 Jan 2020 11:13:20 +0100 Subject: [PATCH 482/771] Abide by lsp when reporting and moving to columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot-current-column-function): Set to eglot-lsp-abiding-column. (eglot-move-to-column-function): Set to eglot-move-to-lsp-abiding-column. * NEWS.md: Log the change here as well. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/361 --- lisp/progmodes/eglot.el | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2a50611d364..5d363ed3037 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1002,15 +1002,15 @@ CONNECT-ARGS are passed as additional arguments to (defun eglot-current-column () (- (point) (point-at-bol))) -(defvar eglot-current-column-function #'eglot-current-column +(defvar eglot-current-column-function #'eglot-lsp-abiding-column "Function to calculate the current column. This is the inverse operation of `eglot-move-to-column-function' (which see). It is a function of no arguments returning a column number. For buffers managed by fully LSP-compliant servers, this should be set to -`eglot-lsp-abiding-column', and `eglot-current-column' (the default) -for all others.") +`eglot-lsp-abiding-column' (the default), and +`eglot-current-column' for all others.") (defun eglot-lsp-abiding-column () "Calculate current COLUMN as defined by the LSP spec." @@ -1026,7 +1026,7 @@ for all others.") :character (progn (when pos (goto-char pos)) (funcall eglot-current-column-function))))) -(defvar eglot-move-to-column-function #'eglot-move-to-column +(defvar eglot-move-to-column-function #'eglot-move-to-lsp-abiding-column "Function to move to a column reported by the LSP server. According to the standard, LSP column/character offsets are based @@ -1036,8 +1036,8 @@ where X is a multi-byte character, it actually means `b', not `c'. However, many servers don't follow the spec this closely. For buffers managed by fully LSP-compliant servers, this should -be set to `eglot-move-to-lsp-abiding-column', and -`eglot-move-to-column' (the default) for all others.") +be set to `eglot-move-to-lsp-abiding-column' (the default), and +`eglot-move-to-column' for all others.") (defun eglot-move-to-column (column) "Move to COLUMN without closely following the LSP spec." From 1c2dc32a6e5a7266371535ab8b71f730cfc1f6f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 16 Apr 2020 10:31:01 +0100 Subject: [PATCH 483/771] * eglot.el (version): bump to 1.6 * NEWS.md: Bump to 1.6 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5d363ed3037..eafe22e224b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018 Free Software Foundation, Inc. -;; Version: 1.5 +;; Version: 1.6 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From 6bfa6e2532c4d81fa717ff7cb3b9a2a1fdce2f19 Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Fri, 3 Jan 2020 21:42:08 +0100 Subject: [PATCH 484/771] Simplify a bit of code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Távora * eglot.el (eglot-move-to-lsp-abiding-column): use already existing function to refer to lsp-abiding-column GitHub-reference: close https://github.com/joaotavora/eglot/issues/397 --- lisp/progmodes/eglot.el | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eafe22e224b..58980a1147e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1012,9 +1012,10 @@ fully LSP-compliant servers, this should be set to `eglot-lsp-abiding-column' (the default), and `eglot-current-column' for all others.") -(defun eglot-lsp-abiding-column () - "Calculate current COLUMN as defined by the LSP spec." - (/ (- (length (encode-coding-region (line-beginning-position) +(defun eglot-lsp-abiding-column (&optional lbp) + "Calculate current COLUMN as defined by the LSP spec. +LBP defaults to `line-beginning-position'." + (/ (- (length (encode-coding-region (or lbp (line-beginning-position)) (point) 'utf-16 t)) 2) 2)) @@ -1057,9 +1058,7 @@ be set to `eglot-move-to-lsp-abiding-column' (the default), and (narrow-to-region lbp (line-end-position)) (move-to-column column) for diff = (- column - (/ (- (length (encode-coding-region lbp (point) 'utf-16 t)) - 2) - 2)) + (eglot-lsp-abiding-column lbp)) until (zerop diff) do (condition-case eob-err (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) From 2a0e0433cd15d3a0b9de220b141c2c12b549c67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Tue, 21 Jan 2020 19:35:34 +0100 Subject: [PATCH 485/771] Update dependencies and copyright years * eglot.el: Update dependencies and copyright years. GitHub-reference: close https://github.com/joaotavora/eglot/issues/413 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58980a1147e..13ee30723bc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,13 +1,13 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2018 Free Software Foundation, Inc. +;; Copyright (C) 2018-2020 Free Software Foundation, Inc. ;; Version: 1.6 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.7") (flymake "1.0.5")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8")) ;; 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 From da888370b4d61025cb9d3d5dbd591b9d04a6f5dd Mon Sep 17 00:00:00 2001 From: Andrii Kolomoiets Date: Thu, 23 Apr 2020 23:41:22 +0300 Subject: [PATCH 486/771] Use text-mode for plaintext markup * eglot.el (eglot--format-markup): Use text-mode for plaintext markup. GitHub-reference: close https://github.com/joaotavora/eglot/issues/444 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13ee30723bc..6d6f91f55aa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1109,6 +1109,7 @@ Doubles as an indicator of snippet support." (list (plist-get markup :value) (pcase (plist-get markup :kind) ("markdown" 'gfm-view-mode) + ("plaintext" 'text-mode) (_ major-mode)))))) (with-temp-buffer (insert string) From 81385edb7132461500f229919c074e329f42b009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 24 Apr 2020 15:39:16 +0100 Subject: [PATCH 487/771] Don't reupdate help buffer if already rendered * eglot.el (eglot--update-doc): Don't reupdate if doc buffer already exists. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/445 --- lisp/progmodes/eglot.el | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d6f91f55aa..ce4f19c65c2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2252,11 +2252,13 @@ potentially rename EGLOT's help buffer." (and eglot-put-doc-in-help-buffer (funcall eglot-put-doc-in-help-buffer string))) (with-current-buffer (eglot--help-buffer) - (rename-buffer (format "*eglot-help for %s*" hint)) - (let ((inhibit-read-only t)) - (erase-buffer) - (insert string) - (goto-char (point-min)) + (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))) (if eglot-auto-display-help-buffer (display-buffer (current-buffer)) (unless (get-buffer-window (current-buffer)) From 23878a9404fcefcc3ffc32bcbf9456a6c2cf06c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 24 Apr 2020 16:33:36 +0100 Subject: [PATCH 488/771] * eglot.el (eglot-put-doc-in-help-buffer): tiny docstring fix. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ce4f19c65c2..3ea839115c1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2231,7 +2231,7 @@ Respects `max-mini-window-height' (which see)." #'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 (usually -the echo area). If t, use `*eglot-help; unconditionally. If a +the echo area). If t, use `*eglot-help*' unconditionally. If a function, it is called with the docstring to display and should a boolean producing one of the two previous values." :type '(choice (const :tag "Never use `*eglot-help*'" nil) From 4d3cf3330671a3df4433d1f35efda779ebb72460 Mon Sep 17 00:00:00 2001 From: Trevor Murphy Date: Fri, 24 Apr 2020 11:52:01 -0700 Subject: [PATCH 489/771] Create match xrefs when possible "Match xrefs" are created with `xref-make-match' instead of `xref-make'. Match xrefs support `xref-query-replace-in-results' from the results buffer. * eglot.el (eglot--xref-make-match): Calculate xref match length from the eglot range. GitHub-reference: close https://github.com/joaotavora/eglot/issues/435 --- lisp/progmodes/eglot.el | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3ea839115c1..c7c45518125 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1833,8 +1833,8 @@ Calls REPORT-FN maybe if server publishes diagnostics in time." (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) (clrhash eglot--temp-location-buffers)))) -(defun eglot--xref-make (name uri range) - "Like `xref-make' but with LSP's NAME, URI and RANGE. +(defun eglot--xref-make-match (name uri range) + "Like `xref-make-match' but with LSP's NAME, URI and RANGE. Try to visit the target file for a richer summary line." (pcase-let* ((file (eglot--uri-to-path uri)) @@ -1849,8 +1849,9 @@ Try to visit the target file for a richer summary line." (hi-end (- (min (point-at-eol) end) bol))) (add-face-text-property hi-beg hi-end 'highlight t substring) - (list substring (1+ (current-line)) (eglot-current-column)))))) - (`(,summary ,line ,column) + (list substring (1+ (current-line)) (eglot-current-column) + (- end beg)))))) + (`(,summary ,line ,column ,length) (cond (visiting (with-current-buffer visiting (funcall collect))) ((file-readable-p file) (with-current-buffer @@ -1859,9 +1860,12 @@ Try to visit the target file for a richer summary line." (insert-file-contents file) (funcall collect))) (t ;; fall back to the "dumb strategy" - (let ((start (cl-getf range :start))) - (list name (1+ (cl-getf start :line)) (cl-getf start :character))))))) - (xref-make summary (xref-make-file-location file line column)))) + (let* ((start (cl-getf range :start)) + (line (1+ (cl-getf start :line))) + (start-pos (cl-getf start :character)) + (end-pos (cl-getf (cl-getf range :end) :character))) + (list name line start-pos (- end-pos start-pos))))))) + (xref-make-match summary (xref-make-file-location file line column) length))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (eglot--error "cannot (yet) provide reliable completion table for LSP symbols")) @@ -1892,7 +1896,7 @@ Try to visit the target file for a richer summary line." (eglot--collecting-xrefs (collect) (mapc (eglot--lambda ((Location) uri range) - (collect (eglot--xref-make (symbol-at-point) uri range))) + (collect (eglot--xref-make-match (symbol-at-point) uri range))) (if (vectorp response) response (list response)))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) @@ -1935,7 +1939,7 @@ Try to visit the target file for a richer summary line." (mapc (eglot--lambda ((SymbolInformation) name location) (eglot--dbind ((Location) uri range) location - (collect (eglot--xref-make name uri range)))) + (collect (eglot--xref-make-match name uri range)))) (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol `(:query ,pattern)))))) From 047f99896c127008287e87583750ac4b159c0696 Mon Sep 17 00:00:00 2001 From: Tobias Rittweiler Date: Sun, 26 Apr 2020 17:30:11 +0200 Subject: [PATCH 490/771] Tests: print contents of *eglot ...* buffers in batch mode. Useful for the CI on github. To be able to see more of the context of a failure. * eglot.el (eglot-server-initialized-hook): Changed semantics. Now called when an instance of `eglot-lsp-server' is created as part of the "connect to server" flow. Previously, there was no difference between this hook and `eglot-connect-hook' which continues to be run once a connection was successfully established. The `eglot-server-initialized-hook' will now capture ALL server instances including those that failed to be started. This change was necessary to make the test suite be able to dump the output of processes that fail to start when running the test suite in batch mode ("make check" and the CI.) In PR https://github.com/joaotavora/eglot/issues/448 it was decided that it is ok to change the semantics of this hook rather than introducing a new hook. (eglot--connect): Change place of where the hook is run. (eglot-connect-hook): Initialized now with `eglot-signal-didChangeConfiguration' which was kept in `eglot-server-initialized-hook' before. * eglot-tests.el (eglot--call-with-fixture): Use `eglot-server-initialized-hook' rather than `eglot-connect-hook'. And dump the contents of the *EGLOT ...* buffers when run in `noninteractive' (i.e. batch) mode. (eglot--cleanup-after-test): New auxiliary function. Extracted verbatim out of `eglot--call-with-fixture` in order to lower the latter's LOC. --- lisp/progmodes/eglot.el | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c7c45518125..13571d4315c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -790,11 +790,19 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose))) (jsonrpc-forget-pending-continuations server)) -(defvar eglot-connect-hook nil "Hook run after connecting in `eglot--connect'.") +(defvar eglot-connect-hook + '(eglot-signal-didChangeConfiguration) + "Hook run after connecting in `eglot--connect'.") (defvar eglot-server-initialized-hook - '(eglot-signal-didChangeConfiguration) - "Hook run after server is successfully initialized. + '() + "Hook run after a `eglot-lsp-server' instance is created. + +That is before a connection was established. Use +`eglot-connect-hook' to hook into when a connection was +successfully established and the server on the other side has +received the initializing configuration. + Each function is passed the server as an argument") (defun eglot--connect (managed-major-mode project class contact) @@ -851,6 +859,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) (setf (eglot--inferior-process server) autostart-inferior-process) + (run-hook-with-args 'eglot-server-initialized-hook server) ;; Now start the handshake. To honour `eglot-sync-connect' ;; maybe-sync-maybe-async semantics we use `jsonrpc-async-request' ;; and mimic most of `jsonrpc-request'. @@ -898,8 +907,7 @@ This docstring appeases checkdoc, that's all." (let ((default-directory (car (project-roots project))) (major-mode managed-major-mode)) (hack-dir-local-variables-non-file-buffer) - (run-hook-with-args 'eglot-connect-hook server) - (run-hook-with-args 'eglot-server-initialized-hook server)) + (run-hook-with-args 'eglot-connect-hook server)) (eglot--message "Connected! Server `%s' now managing `%s' buffers \ in project `%s'." From 60914d2ca402a26f099ab27564611bb4d7e22e67 Mon Sep 17 00:00:00 2001 From: Andrii Kolomoiets Date: Thu, 23 Apr 2020 10:44:12 +0300 Subject: [PATCH 491/771] Hide eldoc-message on empty hover info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Távora * eglot.el (eglot-eldoc-function): Pass nil to eglot--update-doc on empty hover info. (eglot--update-doc): Skip update eglot help buffer if string is nil. GitHub-reference: close https://github.com/joaotavora/eglot/issues/439 --- lisp/progmodes/eglot.el | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13571d4315c..c485b4e2ddd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2259,10 +2259,12 @@ Buffer is displayed with `display-buffer', which obeys (defun eglot--update-doc (string hint) "Put updated documentation STRING where it belongs. Honours `eglot-put-doc-in-help-buffer'. HINT is used to -potentially rename EGLOT's help buffer." - (if (or (eq t eglot-put-doc-in-help-buffer) - (and eglot-put-doc-in-help-buffer - (funcall eglot-put-doc-in-help-buffer string))) +potentially rename EGLOT's help buffer. If STRING is nil, the +echo area cleared of any previous documentation." + (if (and string + (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))) @@ -2314,10 +2316,10 @@ potentially rename EGLOT's help buffer." :success-fn (eglot--lambda ((Hover) contents range) (unless sig-showing (when-buffer-window - (when-let (info (and (not (seq-empty-p contents)) - (eglot--hover-info contents - range))) - (eglot--update-doc info thing-at-point))))) + (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 From bbf8a0d0f65aecdd617ea2d07b7c9e7f4053a79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 2 May 2020 10:30:28 +0100 Subject: [PATCH 492/771] Also check types when destructuring lsp objects The problem in this issue is that the disambiguation between Command and CodeAction objects can only be performed by checking the types of the keys involved. So we added that to the spec and check it at runtime. * eglot.el (eglot--lsp-interface-alist): Add types to Command. Tweak docstring. (eglot--check-object): Renamed from eglot--call-with-interface. (eglot--ensure-type): New helper. (eglot--interface): New helper. (eglot--check-dspec): Renamed from eglot--check-interface. (eglot--dbind): Simplify. (eglot-code-actions): Adjust indentation. * eglot-tests.el (eglot-dcase-issue-452): New test. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/452 --- lisp/progmodes/eglot.el | 118 +++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c485b4e2ddd..42fca9be526 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -231,7 +231,7 @@ let the buffer grow forever." `( (CodeAction (:title) (:kind :diagnostics :edit :command)) (ConfigurationItem () (:scopeUri :section)) - (Command (:title :command) (:arguments)) + (Command ((:title . string) (:command . string)) (:arguments)) (CompletionItem (:label) (:kind :detail :documentation :deprecated :preselect :sortText :filterText :insertText :insertTextFormat @@ -265,13 +265,15 @@ let the buffer grow forever." INTERFACE-NAME is a symbol designated by the spec as \"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where -REQUIRED and OPTIONAL are lists of keyword symbols designating -field names that must be, or may be, respectively, present in a -message adhering to that interface. +REQUIRED and OPTIONAL are lists of KEYWORD designating field +names that must be, or may be, respectively, present in a message +adhering to that interface. KEY can be a keyword or a cons (SYM +TYPE), where type is used by `cl-typep' to check types at +runtime. Here's what an element of this alist might look like: - (CreateFile . ((:kind :uri) (:options)))")) + (Command ((:title . string) (:command . string)) (:arguments))")) (eval-and-compile (defvar eglot-strict-mode (if load-file-name '() @@ -308,46 +310,69 @@ on unknown notifications and errors on unknown requests. (defun eglot--plist-keys (plist) (cl-loop for (k _v) on plist by #'cddr collect k)) -(defun eglot--call-with-interface (interface object fn) - "Call FN, checking that OBJECT conforms to INTERFACE." - (when-let ((missing (and (memq 'enforce-required-keys eglot-strict-mode) - (cl-set-difference (car (cdr interface)) - (eglot--plist-keys object))))) - (eglot--error "A `%s' must have %s" (car interface) missing)) - (when-let ((excess (and (memq 'disallow-non-standard-keys eglot-strict-mode) - (cl-set-difference - (eglot--plist-keys object) - (append (car (cdr interface)) (cadr (cdr interface))))))) - (eglot--error "A `%s' mustn't have %s" (car interface) excess)) - (funcall fn)) +(cl-defun eglot--check-object (interface-name + object + &optional + (enforce-required t) + (disallow-non-standard t) + (check-types t)) + "Check that OBJECT conforms to INTERFACE. Error otherwise." + (cl-destructuring-bind + (&key types required-keys optional-keys &allow-other-keys) + (eglot--interface interface-name) + (when-let ((missing (and enforce-required + (cl-set-difference required-keys + (eglot--plist-keys object))))) + (eglot--error "A `%s' must have %s" interface-name missing)) + (when-let ((excess (and disallow-non-standard + (cl-set-difference + (eglot--plist-keys object) + (append required-keys optional-keys))))) + (eglot--error "A `%s' mustn't have %s" interface-name excess)) + (when check-types + (cl-loop + for (k v) on object by #'cddr + for type = (or (cdr (assoc k types)) t) ;; FIXME: enforce nil type? + unless (cl-typep v type) + do (eglot--error "A `%s' must have a %s as %s, but has %s" + interface-name ))) + t)) (eval-and-compile (defun eglot--keywordize-vars (vars) (mapcar (lambda (var) (intern (format ":%s" var))) vars)) - (defun eglot--check-interface (interface-name vars) - (let ((interface - (assoc interface-name eglot--lsp-interface-alist))) - (cond (interface + (defun eglot--ensure-type (k) (if (consp k) k (cons k t))) + + (defun eglot--interface (interface-name) + (let* ((interface (assoc interface-name eglot--lsp-interface-alist)) + (required (mapcar #'eglot--ensure-type (car (cdr interface)))) + (optional (mapcar #'eglot--ensure-type (cadr (cdr interface))))) + (list :types (append required optional) + :required-keys (mapcar #'car required) + :optional-keys (mapcar #'car optional)))) + + (defun eglot--check-dspec (interface-name dspec) + "Check if variables in DSPEC " + (cl-destructuring-bind (&key required-keys optional-keys &allow-other-keys) + (eglot--interface interface-name) + (cond ((or required-keys optional-keys) (let ((too-many (and (memq 'disallow-non-standard-keys eglot-strict-mode) (cl-set-difference - (eglot--keywordize-vars vars) - (append (car (cdr interface)) - (cadr (cdr interface)))))) + (eglot--keywordize-vars dspec) + (append required-keys optional-keys)))) (ignored-required (and (memq 'enforce-required-keys eglot-strict-mode) (cl-set-difference - (car (cdr interface)) - (eglot--keywordize-vars vars)))) + required-keys (eglot--keywordize-vars dspec)))) (missing-out (and (memq 'enforce-optional-keys eglot-strict-mode) (cl-set-difference - (cadr (cdr interface)) - (eglot--keywordize-vars vars))))) + optional-keys (eglot--keywordize-vars dspec))))) (when too-many (byte-compile-warn "Destructuring for %s has extraneous %s" interface-name too-many)) @@ -361,7 +386,7 @@ on unknown notifications and errors on unknown requests. (byte-compile-warn "Unknown LSP interface %s" interface-name)))))) (cl-defmacro eglot--dbind (vars object &body body) - "Destructure OBJECT of binding VARS in BODY. + "Destructure OBJECT, binding VARS in BODY. VARS is ([(INTERFACE)] SYMS...) Honour `eglot-strict-mode'." (declare (indent 2) (debug (sexp sexp &rest form))) @@ -370,13 +395,14 @@ Honour `eglot-strict-mode'." (object-once (make-symbol "object-once")) (fn-once (make-symbol "fn-once"))) (cond (interface-name - (eglot--check-interface interface-name vars) + (eglot--check-dspec interface-name vars) `(let ((,object-once ,object)) (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once - (eglot--call-with-interface (assoc ',interface-name - eglot--lsp-interface-alist) - ,object-once (lambda () - ,@body))))) + (eglot--check-object ',interface-name ,object-once + (memq 'enforce-required-keys eglot-strict-mode) + (memq 'disallow-non-standard-keys eglot-strict-mode) + (memq 'check-types eglot-strict-mode)) + ,@body))) (t `(let ((,object-once ,object) (,fn-once (lambda (,@vars) ,@body))) @@ -409,20 +435,12 @@ treated as in `eglot-dbind'." (car (pop vars))) for condition = (cond (interface-name - (eglot--check-interface interface-name vars) + (eglot--check-dspec interface-name vars) ;; In this mode, in runtime, we assume ;; `eglot-strict-mode' is fully on, otherwise we ;; can't disambiguate between certain types. - `(let* ((interface - (or (assoc ',interface-name eglot--lsp-interface-alist) - (eglot--error "Unknown LSP interface %s" - ',interface-name))) - (object-keys (eglot--plist-keys ,obj-once)) - (required-keys (car (cdr interface)))) - (and (null (cl-set-difference required-keys object-keys)) - (null (cl-set-difference - (cl-set-difference object-keys required-keys) - (cadr (cdr interface))))))) + `(ignore-errors + (eglot--check-object ',interface-name ,obj-once))) (t ;; In this interface-less mode we don't check ;; `eglot-strict-mode' at all: just check that the object @@ -435,7 +453,7 @@ treated as in `eglot-dbind'." ,obj-once ,@body))) (t - (eglot--error "%s didn't match any of %s" + (eglot--error "%S didn't match any of %S" ,obj-once ',(mapcar #'car clauses))))))) @@ -2499,12 +2517,12 @@ echo area cleared of any previous documentation." (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) (cdr (assoc (completing-read "[eglot] Pick an action: " - menu-items nil t - nil nil (car menu-items)) + menu-items nil t + nil nil (car menu-items)) menu-items))))) (eglot--dcase action - (((Command) command arguments) - (eglot-execute-command server (intern command) arguments)) + (((Command) command arguments) + (eglot-execute-command server (intern command) arguments)) (((CodeAction) edit command) (when edit (eglot--apply-workspace-edit edit)) (when command From ed162088f3036cbb9e964cc2775a2e12c0b6e9c4 Mon Sep 17 00:00:00 2001 From: Ingo Lohmar Date: Fri, 20 Sep 2019 18:39:23 +0200 Subject: [PATCH 493/771] Support hierarchical documentsymbol in eglot-imenu A reworking of an original implementation by Ingo Lohmar * eglot.el (eglot-client-capabilities, defvar): Add DocumentSymbol. (eglot-client-capabilities): Add :hierarchicalDocumentSymbolSupport. (eglot--parse-DocumentSymbol): Remove. (eglot-imenu): Rewrite. * NEWS.md (1.7): Mention new feature GitHub-reference: close https://github.com/joaotavora/eglot/issues/303 --- lisp/progmodes/eglot.el | 79 +++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 42fca9be526..afb7063c64c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -56,6 +56,7 @@ ;;; Code: (require 'json) +(require 'imenu) (require 'cl-lib) (require 'project) (require 'url-parse) @@ -255,7 +256,12 @@ let the buffer grow forever." (ShowMessageRequestParams (:type :message) (:actions)) (SignatureHelp (:signatures) (:activeSignature :activeParameter)) (SignatureInformation (:label) (:documentation :parameters)) - (SymbolInformation (:name :kind :location) (:deprecated :containerName)) + (SymbolInformation (:name :kind :location) + (:deprecated :containerName)) + (DocumentSymbol (:name :range :selectionRange :kind) + ;; `:containerName' isn't really allowed , but + ;; it simplifies the impl of `eglot-imenu'. + (:detail :deprecated :children :containerName)) (TextDocumentEdit (:textDocument :edits) ()) (TextEdit (:range :newText)) (VersionedTextDocumentIdentifier (:uri :version) ()) @@ -532,6 +538,7 @@ treated as in `eglot-dbind'." :typeDefinition `(:dynamicRegistration :json-false) :documentSymbol (list :dynamicRegistration :json-false + :hierarchicalDocumentSymbolSupport t :symbolKind `(:valueSet [,@(mapcar #'car eglot--symbol-kind-names)])) @@ -2361,36 +2368,48 @@ echo area cleared of any previous documentation." (defun eglot-imenu () "EGLOT's `imenu-create-index-function'." - (let ((entries - (and - (eglot--server-capable :documentSymbolProvider) - (mapcar - (eglot--lambda - ((SymbolInformation) name kind location containerName) - (cons (propertize - name - :kind (alist-get kind eglot--symbol-kind-names - "Unknown") - :containerName (and (stringp containerName) - (not (string-empty-p containerName)) - containerName)) - (eglot--lsp-position-to-point - (plist-get (plist-get location :range) :start)))) - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/documentSymbol - `(:textDocument ,(eglot--TextDocumentIdentifier))))))) + (cl-labels + ((visit (_name one-obj-array) + (imenu-default-goto-function + nil (car (eglot--range-region + (eglot--dcase (aref one-obj-array 0) + (((SymbolInformation) location) + (plist-get location :range)) + (((DocumentSymbol) selectionRange) + selectionRange)))))) + (unfurl (obj) + (eglot--dcase obj + (((SymbolInformation)) (list obj)) + (((DocumentSymbol) name children) + (cons obj + (mapcar + (lambda (c) + (plist-put + c :containerName + (let ((existing (plist-get c :containerName))) + (if existing (format "%s::%s" name existing) + name)))) + (mapcan #'unfurl children))))))) (mapcar - (pcase-lambda (`(,kind . ,syms)) - (let ((syms-by-scope (seq-group-by - (lambda (e) - (get-text-property 0 :containerName (car e))) - syms))) - (cons kind (cl-loop for (scope . elems) in syms-by-scope - append (if scope - (list (cons scope elems)) - elems))))) - (seq-group-by (lambda (e) (get-text-property 0 :kind (car e))) - entries)))) + (pcase-lambda (`(,kind . ,objs)) + (cons + (alist-get kind eglot--symbol-kind-names "Unknown") + (mapcan (pcase-lambda (`(,container . ,objs)) + (let ((elems (mapcar (lambda (obj) + (list (plist-get obj :name) + `[,obj] ;; trick + #'visit)) + objs))) + (if container (list (cons container elems)) elems))) + (seq-group-by + (lambda (e) (plist-get e :containerName)) objs)))) + (seq-group-by + (lambda (obj) (plist-get obj :kind)) + (mapcan #'unfurl + (jsonrpc-request (eglot--current-server-or-lose) + :textDocument/documentSymbol + `(:textDocument + ,(eglot--TextDocumentIdentifier)))))))) (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." From 3773b2638f35e39c7040ba86497dcfe62bff1217 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sat, 25 Apr 2020 22:26:14 -0400 Subject: [PATCH 494/771] Tweak docstring of eglot-server-programs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Távora * eglot.el (eglot-server-programs): Fix typos and phrasing. --- lisp/progmodes/eglot.el | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index afb7063c64c..6db0a09f8f8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -120,27 +120,28 @@ of those modes. CONTACT can be: PROGRAM is called with ARGS and is expected to serve LSP requests over the standard input/output channels. -* A list (HOST PORT [TCP-ARGS...]) where HOST is a string and PORT is - na positive integer number for connecting to a server via TCP. +* A list (HOST PORT [TCP-ARGS...]) where HOST is a string and + PORT is a positive integer for connecting to a server via TCP. Remaining ARGS are passed to `open-network-stream' for upgrading the connection with encryption or other capabilities. -* A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereby a - combination of the two previous options is used.. First, an +* A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereupon a + combination of the two previous options is used. First, an attempt is made to find an available server port, then PROGRAM is launched with ARGS; the `:autoport' keyword substituted for - that number; and MOREARGS. Eglot then attempts to to establish - a TCP connection to that port number on the localhost. + that number; and MOREARGS. Eglot then attempts to establish a + TCP connection to that port number on the localhost. * A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol designating a subclass of `eglot-lsp-server', for representing experimental LSP servers. INITARGS is a keyword-value plist - used to initialize CLASS-NAME, or a plain list interpreted as - the previous descriptions of CONTACT, in which case it is - converted to produce a plist with a suitable :PROCESS initarg - to CLASS-NAME. The class `eglot-lsp-server' descends - `jsonrpc-process-connection', which you should see for the - semantics of the mandatory :PROCESS argument. + used to initialize the object of CLASS-NAME, or a plain list + interpreted as the previous descriptions of CONTACT. In the + latter case that plain list is used to produce a plist with a + suitable :PROCESS initarg to CLASS-NAME. The class + `eglot-lsp-server' descends from `jsonrpc-process-connection', + which you should see for the semantics of the mandatory + :PROCESS argument. * A function of a single argument producing any of the above values for CONTACT. The argument's value is non-nil if the From e91a40007682491e2f675a6a84151564baaf5c39 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Thu, 30 Apr 2020 20:04:24 -0400 Subject: [PATCH 495/771] Unbreak eglot--guess-contact for host-and-port case * eglot.el (eglot--guess-contact): Fix bug in (host port) connection case. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/446 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6db0a09f8f8..adfd4e29f8c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -698,7 +698,8 @@ be guessed." (class (or (and (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) - (program (and (listp guess) (stringp (car guess)) (car guess))) + (program (and (listp guess) + (stringp (car guess)) (stringp (cadr guess)) (car guess))) (base-prompt (and interactive "Enter program to execute (or :): ")) From d285e0060a721d84a35a06c1e9784cd8d8756c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 3 May 2020 00:43:00 +0100 Subject: [PATCH 496/771] Kind of honour eldoc-echo-area-use-multiline-p A reworking of an idea and original implementation by Andrii Kolomoiets . It doesn't honor it completely because the semantics for a non-t, non-nil value are tricky. And we don't always exactly know what the symbol prefix reliably. * eglot.el (eglot--update-doc): Kind of honour eldoc-echo-area-use-multiline-p. GitHub-reference: close https://github.com/joaotavora/eglot/issues/443 --- lisp/progmodes/eglot.el | 53 +++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index adfd4e29f8c..436e5bfe445 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2288,29 +2288,36 @@ Buffer is displayed with `display-buffer', which obeys Honours `eglot-put-doc-in-help-buffer'. HINT is used to potentially rename EGLOT's help buffer. If STRING is nil, the echo area cleared of any previous documentation." - (if (and string - (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))) - (if eglot-auto-display-help-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - (eglot--message - "%s\n(...truncated. Full help is in `%s')" - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...") - (buffer-name eglot--help-buffer)))) - (help-mode))) - (eldoc-message string))) + (cond ((and string + (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))) + (if eglot-auto-display-help-buffer + (display-buffer (current-buffer)) + (unless (get-buffer-window (current-buffer)) + (eglot--message + "%s\n(...truncated. Full help is in `%s')" + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...") + (buffer-name eglot--help-buffer)))) + (help-mode)))) + (eldoc-echo-area-use-multiline-p + (eldoc-message string)) + (t + (eldoc-message + (and string + (if (string-match "\n" string) + (substring string (match-end 0)) + string)))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." From a88cc9210bf9ca31003954a6af043a08c462fc1c Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Wed, 29 Apr 2020 10:09:24 +0200 Subject: [PATCH 497/771] Always string-trim markup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Távora * eglot.el: (eglot--format-markup): Factor string trim out so we string-trim for all cases GitHub-reference: close https://github.com/joaotavora/eglot/issues/450 --- lisp/progmodes/eglot.el | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 436e5bfe445..bdc4cd0aaab 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1139,15 +1139,14 @@ Doubles as an indicator of snippet support." (defun eglot--format-markup (markup) "Format MARKUP according to LSP's spec." (pcase-let ((`(,string ,mode) - (if (stringp markup) (list (string-trim markup) - (intern "gfm-view-mode")) + (if (stringp markup) (list markup 'gfm-view-mode) (list (plist-get markup :value) (pcase (plist-get markup :kind) ("markdown" 'gfm-view-mode) ("plaintext" 'text-mode) (_ major-mode)))))) (with-temp-buffer - (insert string) + (insert (string-trim string)) (ignore-errors (delay-mode-hooks (funcall mode))) (font-lock-ensure) (buffer-string)))) From 5d00eac56495ce6786f374785cab034ff82962fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Sat, 11 Jan 2020 19:08:59 +0100 Subject: [PATCH 498/771] Declare markdown support iff gfm-view-mode installed * eglot.el (eglot-client-capabilities): Support markdown only when gfm-view-mode is installed. GitHub-reference: close https://github.com/joaotavora/eglot/issues/408 --- lisp/progmodes/eglot.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bdc4cd0aaab..596a82d6a80 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -527,7 +527,10 @@ treated as in `eglot-dbind'." :json-false)) :contextSupport t) :hover (list :dynamicRegistration :json-false - :contentFormat ["markdown" "plaintext"]) + :contentFormat + (if (fboundp 'gfm-view-mode) + ["markdown" "plaintext"] + ["plaintext"])) :signatureHelp (list :dynamicRegistration :json-false :signatureInformation `(:parameterInformation From c8efef647e2705665a8e3b9de3eedccc209fc981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 3 May 2020 01:00:04 +0100 Subject: [PATCH 499/771] Fontify markdown source code blocks by default * eglot.el (eglot--format-markup): Set markdown-fontify-code-blocks-natively to t locally. GitHub-reference: per https://github.com/joaotavora/eglot/issues/408 --- lisp/progmodes/eglot.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 596a82d6a80..22a8aefa311 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -72,7 +72,9 @@ (require 'filenotify) (require 'ert) (require 'array) -(defvar company-backends) ; forward-declare, but don't require company +;; forward-declare, but don't require (Emacs 28 doesn't seem to care) +(defvar markdown-fontify-code-blocks-natively) +(defvar company-backends) (defvar company-tooltip-align-annotations) @@ -1149,6 +1151,7 @@ Doubles as an indicator of snippet support." ("plaintext" 'text-mode) (_ major-mode)))))) (with-temp-buffer + (setq-local markdown-fontify-code-blocks-natively t) (insert (string-trim string)) (ignore-errors (delay-mode-hooks (funcall mode))) (font-lock-ensure) From 1914356c60e7308175121dbae8340dbbc9847ffc Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Sun, 3 May 2020 11:20:27 +0200 Subject: [PATCH 500/771] Tweak handling of eldoc-echo-area-use-multiline-p MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also close https://github.com/joaotavora/eglot/issues/453 Co-authored-by: João Távora * eglot.el (eglot--first-line-of-doc): New helper. (eglot--update-doc): Tweak docstring. Simplify. (eglot-put-doc-in-help-buffer): Tweak docstring GitHub-reference: per https://github.com/joaotavora/eglot/issues/443 --- lisp/progmodes/eglot.el | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22a8aefa311..09be1f8dc61 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2274,10 +2274,11 @@ Respects `max-mini-window-height' (which see)." (defcustom eglot-put-doc-in-help-buffer #'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 (usually -the echo area). If t, use `*eglot-help*' unconditionally. If a -function, it is called with the docstring to display and should a -boolean producing one of the two previous values." +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 docstring +to display and should a boolean producing one of the two previous +values." :type '(choice (const :tag "Never use `*eglot-help*'" nil) (const :tag "Always use `*eglot-help*'" t) (function :tag "Ask a function"))) @@ -2288,15 +2289,22 @@ Buffer is displayed with `display-buffer', which obeys `display-buffer-alist' & friends." :type 'boolean) +(defun eglot--first-line-of-doc (string) + (truncate-string-to-width + (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) + (frame-width) nil nil "...")) + (defun eglot--update-doc (string hint) "Put updated documentation STRING where it belongs. -Honours `eglot-put-doc-in-help-buffer'. HINT is used to -potentially rename EGLOT's help buffer. If STRING is nil, the -echo area cleared of any previous documentation." - (cond ((and string - (or (eq t eglot-put-doc-in-help-buffer) - (and eglot-put-doc-in-help-buffer - (funcall eglot-put-doc-in-help-buffer string)))) +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))) @@ -2310,19 +2318,14 @@ echo area cleared of any previous documentation." (unless (get-buffer-window (current-buffer)) (eglot--message "%s\n(...truncated. Full help is in `%s')" - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...") + (eglot--first-line-of-doc string) (buffer-name eglot--help-buffer)))) (help-mode)))) (eldoc-echo-area-use-multiline-p + ;; Can't really honour non-t non-nil values if this var (eldoc-message string)) (t - (eldoc-message - (and string - (if (string-match "\n" string) - (substring string (match-end 0)) - string)))))) + (eldoc-message (eglot--first-line-of-doc string))))) (defun eglot-eldoc-function () "EGLOT's `eldoc-documentation-function' function." From a2af2e126817cff818d8d56ceb6c689cd7fb025d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 3 May 2020 21:10:05 +0100 Subject: [PATCH 501/771] Remap display-local-help (c-h .) to eglot-help-at-point * eglot.el (eglot-help-at-point): Fallback to display-local-help if no hover doc (eglot-mode-map): Remap display-local-help to eglot-help-at-point.. GitHub-reference: per https://github.com/joaotavora/eglot/issues/437 --- lisp/progmodes/eglot.el | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 09be1f8dc61..8fadd5f7ab3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1238,7 +1238,10 @@ and just return it. PROMPT shouldn't end with a question mark." ;;; Minor modes ;;; -(defvar eglot-mode-map (make-sparse-keymap)) +(defvar eglot-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [remap display-local-help] 'eglot-help-at-point) + map)) (defvar-local eglot--current-flymake-report-fn nil "Current flymake report function for this buffer") @@ -2247,19 +2250,20 @@ is not active." (setq eglot--help-buffer (generate-new-buffer "*eglot-help*")))) (defun eglot-help-at-point () - "Request \"hover\" information for the thing 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)) - (when (seq-empty-p contents) (eglot--error "No hover info here")) - (let ((blurb (eglot--hover-info contents range)) - (sym (thing-at-point 'symbol))) - (with-current-buffer (eglot--help-buffer) - (with-help-window (current-buffer) - (rename-buffer (format "*eglot-help for %s*" sym)) - (with-current-buffer standard-output (insert blurb)) - (setq-local nobreak-char-display nil)))))) + (if (seq-empty-p contents) + (display-local-help) + (let ((blurb (eglot--hover-info contents range)) + (sym (thing-at-point 'symbol))) + (with-current-buffer (eglot--help-buffer) + (with-help-window (current-buffer) + (rename-buffer (format "*eglot-help for %s*" sym)) + (with-current-buffer standard-output (insert blurb)) + (setq-local nobreak-char-display nil))))))) (defun eglot-doc-too-large-for-echo-area (string) "Return non-nil if STRING won't fit in echo area. From 3cef1072ad4973c5055e6719deac27b9d9efc5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 3 May 2020 21:37:42 +0100 Subject: [PATCH 502/771] Survive hover responses with empty markdown strings * eglot.el (eglot-help-at-point): Protect against null eglot--hover-info GitHub-reference: fix https://github.com/joaotavora/eglot/issues/433 --- lisp/progmodes/eglot.el | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8fadd5f7ab3..d172f4c7cc8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2255,15 +2255,16 @@ is not active." (eglot--dbind ((Hover) contents range) (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) - (if (seq-empty-p contents) - (display-local-help) - (let ((blurb (eglot--hover-info contents range)) - (sym (thing-at-point 'symbol))) - (with-current-buffer (eglot--help-buffer) - (with-help-window (current-buffer) - (rename-buffer (format "*eglot-help for %s*" sym)) - (with-current-buffer standard-output (insert blurb)) - (setq-local nobreak-char-display nil))))))) + (let ((blurb (and (not (seq-empty-p contents)) + (eglot--hover-info contents range)))) + (if blurb + (with-current-buffer (eglot--help-buffer) + (with-help-window (current-buffer) + (rename-buffer (format "*eglot-help for %s*" + (thing-at-point 'symbol))) + (with-current-buffer standard-output (insert blurb)) + (setq-local nobreak-char-display nil))) + (display-local-help))))) (defun eglot-doc-too-large-for-echo-area (string) "Return non-nil if STRING won't fit in echo area. From 1091226b3a65d9e4a324691a65db46b47e76d9b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 3 May 2020 21:59:29 +0100 Subject: [PATCH 503/771] Simplify bug-reporting instructions We assume the user has a recent enough jsonrpc.el that consolidates events and stderr int the same transcript. * README.md (Reporting bugs): Simplify instructions. * eglot.el (eglot-events-buffer): Can work with no server. --- lisp/progmodes/eglot.el | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d172f4c7cc8..03609a128c3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -808,9 +808,15 @@ INTERACTIVE is t if called interactively." (add-hook 'post-command-hook #'maybe-connect 'append nil))))) (defun eglot-events-buffer (server) - "Display events buffer for SERVER." - (interactive (list (eglot--current-server-or-lose))) - (display-buffer (jsonrpc-events-buffer server))) + "Display events buffer for SERVER. +Use current server's or first available Eglot events buffer." + (interactive (list eglot--cached-server)) + (let ((buffer (if server (jsonrpc-events-buffer server) + (cl-find "\\*EGLOT.*events\\*" + (buffer-list) + :key #'buffer-name :test #'string-match)))) + (if buffer (display-buffer buffer) + (eglot--error "Can't find an Eglot events buffer!")))) (defun eglot-stderr-buffer (server) "Display stderr buffer for SERVER." From 8cf6f4f292ec8ae469daa80e1b155240d845d73b Mon Sep 17 00:00:00 2001 From: Andrii Kolomoiets Date: Thu, 7 May 2020 00:35:48 +0300 Subject: [PATCH 504/771] Remove trailing whitespaces * eglot.el (defvar company-backends, eglot-code-actions): Remove trailing whitespace --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 03609a128c3..b5f05ce7e31 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -74,7 +74,7 @@ (require 'array) ;; forward-declare, but don't require (Emacs 28 doesn't seem to care) (defvar markdown-fontify-code-blocks-natively) -(defvar company-backends) +(defvar company-backends) (defvar company-tooltip-align-annotations) @@ -2563,7 +2563,7 @@ documentation. Honour `eglot-put-doc-in-help-buffer', (menu `("Eglot code actions:" ("dummy" ,@menu-items))) (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) - (cdr (assoc (completing-read "[eglot] Pick an action: " + (cdr (assoc (completing-read "[eglot] Pick an action: " menu-items nil t nil nil (car menu-items)) menu-items))))) From 11c911574972976fbf39cdfb392d469d61f688da Mon Sep 17 00:00:00 2001 From: Tobias Rittweiler Date: Fri, 8 May 2020 00:51:44 +0200 Subject: [PATCH 505/771] Fix "free variable" warning * eglot.el (eglot-events-buffer): Use `eglot-current-server' instead of `eglot--cached-server' because the latter is declared later in the file. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/460 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b5f05ce7e31..8dae0b70263 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -810,7 +810,7 @@ INTERACTIVE is t if called interactively." (defun eglot-events-buffer (server) "Display events buffer for SERVER. Use current server's or first available Eglot events buffer." - (interactive (list eglot--cached-server)) + (interactive (list (eglot-current-server))) (let ((buffer (if server (jsonrpc-events-buffer server) (cl-find "\\*EGLOT.*events\\*" (buffer-list) From f97e9aa75c794f11442167c239439244c88d343f Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Tue, 12 May 2020 11:50:56 -0400 Subject: [PATCH 506/771] Don't call flymake report function if flymake is disabled Also fix https://github.com/joaotavora/eglot/issues/472. Copyright-paperwork-exempt: yes * eglot.el (eglot-handle-notification): Check that flymake-mode is active before calling flymake report function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/468 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8dae0b70263..112959b3564 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1580,7 +1580,7 @@ COMMAND is a symbol naming the command." (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) into diags - finally (cond (eglot--current-flymake-report-fn + finally (cond ((and flymake-mode eglot--current-flymake-report-fn) (funcall eglot--current-flymake-report-fn diags ;; If the buffer hasn't changed since last ;; call to the report function, flymake won't From cc8bbf69ffbafc99296e5ccce17d39ea945cde63 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Wed, 13 May 2020 20:06:35 -0400 Subject: [PATCH 507/771] Prompt for executable if supplied name does not exist * eglot.el (eglot--guess-contact): Interpret a list containing a single string as an executable when forming the interactive prompt. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/474 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/478 --- lisp/progmodes/eglot.el | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 112959b3564..8a1d162c6d9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -704,7 +704,11 @@ be guessed." (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) (program (and (listp guess) - (stringp (car guess)) (stringp (cadr guess)) (car guess))) + (stringp (car guess)) + ;; A second element might be the port of a (host, port) + ;; pair, but in that case it is not a string. + (or (null (cdr guess)) (stringp (cadr guess))) + (car guess))) (base-prompt (and interactive "Enter program to execute (or :): ")) From c57ee29fb45dd06a71b9acb87712397f38ca22d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 14 May 2020 23:10:48 +0100 Subject: [PATCH 508/771] Require xref, project and eldoc from gnu elpa * Makefile (ELPADEPS): Install Xref, Project and Eldoc. * eglot.el (Package-Requires): Require Xref, Project and Eldoc from GNU ELPA. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8a1d162c6d9..f3501cbbcf2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.8") (package "0.1.1") (xref "1.0.1") (eldoc "1.0.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 From a5a1559e7a5906575325b72cae217a805be1a4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 18 May 2020 13:10:13 +0100 Subject: [PATCH 509/771] Correctly place diagnostics in narrowed buffers * eglot.el (eglot--lsp-position-to-point) (eglot-handle-notification): save-restriction and widen GitHub-reference: fix https://github.com/joaotavora/eglot/issues/479 --- lisp/progmodes/eglot.el | 44 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f3501cbbcf2..21a2496572f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1118,19 +1118,21 @@ be set to `eglot-move-to-lsp-abiding-column' (the default), and "Convert LSP position POS-PLIST to Emacs point. If optional MARKER, return a marker instead" (save-excursion - (goto-char (point-min)) - (forward-line (min most-positive-fixnum - (plist-get pos-plist :line))) - (unless (eobp) ;; if line was excessive leave point at eob - (let ((tab-width 1) - (col (plist-get pos-plist :character))) - (unless (wholenump col) - (eglot--warn - "Caution: LSP server sent invalid character position %s. Using 0 instead." - col) - (setq col 0)) - (funcall eglot-move-to-column-function col))) - (if marker (copy-marker (point-marker)) (point)))) + (save-restriction + (widen) + (goto-char (point-min)) + (forward-line (min most-positive-fixnum + (plist-get pos-plist :line))) + (unless (eobp) ;; if line was excessive leave point at eob + (let ((tab-width 1) + (col (plist-get pos-plist :character))) + (unless (wholenump col) + (eglot--warn + "Caution: LSP server sent invalid character position %s. Using 0 instead." + col) + (setq col 0)) + (funcall eglot-move-to-column-function col))) + (if marker (copy-marker (point-marker)) (point))))) (defun eglot--path-to-uri (path) "URIfy PATH." @@ -1585,13 +1587,15 @@ COMMAND is a symbol naming the command." message `((eglot-lsp-diag . ,diag-spec))))) into diags finally (cond ((and flymake-mode eglot--current-flymake-report-fn) - (funcall eglot--current-flymake-report-fn diags - ;; If the buffer hasn't changed since last - ;; call to the report function, flymake won't - ;; delete old diagnostics. Using :region - ;; keyword forces flymake to delete - ;; them (github#159). - :region (cons (point-min) (point-max))) + (save-restriction + (widen) + (funcall eglot--current-flymake-report-fn diags + ;; If the buffer hasn't changed since last + ;; call to the report function, flymake won't + ;; delete old diagnostics. Using :region + ;; keyword forces flymake to delete + ;; them (github#159). + :region (cons (point-min) (point-max)))) (setq eglot--unreported-diagnostics nil)) (t (setq eglot--unreported-diagnostics (cons t diags)))))) From 6cc6392546a69efaa3089790dcd2d5d0815ae2b3 Mon Sep 17 00:00:00 2001 From: Rudolf Schlatte Date: Thu, 21 May 2020 11:20:41 +0200 Subject: [PATCH 510/771] Add support for erlang_ls * README.md: Mention erlang_ls * eglot.el (eglot-server-programs): Add erlang_ls GitHub-reference: close https://github.com/joaotavora/eglot/issues/471 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 21a2496572f..c38620edf96 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -111,7 +111,8 @@ language-server/bin/php-language-server.php")) (ada-mode . ("ada_language_server")) (scala-mode . ("metals-emacs")) ((tex-mode context-mode texinfo-mode bibtex-mode) - . ("digestif"))) + . ("digestif")) + (erlang-mode . ("erlang_ls" "--transport" "stdio"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From b2dd04ddbe94b5a10884fd01a8309a0ea8f6b887 Mon Sep 17 00:00:00 2001 From: Philipp Stephani Date: Mon, 25 May 2020 11:30:32 +0200 Subject: [PATCH 511/771] Fix type error in eglot--xref-make-match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Its first argument is passed to xref-make-match, which expects a string as its SUMMARY argument, but symbol-at-point returns a symbol. Co-authored-by: João Távora * eglot.el (eglot--lsp-xrefs-for-method): use symbol-name. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/488 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c38620edf96..15fa2a189fe 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1954,7 +1954,8 @@ Try to visit the target file for a richer summary line." (eglot--collecting-xrefs (collect) (mapc (eglot--lambda ((Location) uri range) - (collect (eglot--xref-make-match (symbol-at-point) uri range))) + (collect (eglot--xref-make-match (symbol-name (symbol-at-point)) + uri range))) (if (vectorp response) response (list response)))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) From 43f9294773a9d8928d179c7a4d206a312b869a90 Mon Sep 17 00:00:00 2001 From: Andrii Kolomoiets Date: Wed, 6 May 2020 22:06:35 +0300 Subject: [PATCH 512/771] Rework computation of string given to eldoc (again) Co-authored-by: Andreii Kolomoiets 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 https://github.com/joaotavora/eglot/issues/459 --- lisp/progmodes/eglot.el | 68 ++++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 15fa2a189fe..cc6029079af 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2282,24 +2282,47 @@ is not active." (setq-local nobreak-char-display nil))) (display-local-help))))) -(defun eglot-doc-too-large-for-echo-area (string) - "Return non-nil if STRING won't fit in echo area. -Respects `max-mini-window-height' (which see)." - (let ((max-height - (cond ((floatp max-mini-window-height) (* (frame-height) - max-mini-window-height)) - ((integerp max-mini-window-height) max-mini-window-height) - (t 1)))) - (> (cl-count ?\n string) max-height))) +(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 docstring -to display and should a boolean producing one of the two previous -values." +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"))) @@ -2310,11 +2333,6 @@ Buffer is displayed with `display-buffer', which obeys `display-buffer-alist' & friends." :type 'boolean) -(defun eglot--first-line-of-doc (string) - (truncate-string-to-width - (replace-regexp-in-string "\\(.*\\)\n.*" "\\1" string) - (frame-width) nil nil "...")) - (defun eglot--update-doc (string hint) "Put updated documentation STRING where it belongs. HINT is used to potentially rename EGLOT's help buffer. If @@ -2337,16 +2355,22 @@ documentation. Honour `eglot-put-doc-in-help-buffer', (if eglot-auto-display-help-buffer (display-buffer (current-buffer)) (unless (get-buffer-window (current-buffer)) + ;; This prints two lines. Should it print 1? Or + ;; honour max-mini-window-height? (eglot--message "%s\n(...truncated. Full help is in `%s')" - (eglot--first-line-of-doc string) + (eglot--truncate-string string 1 (- (frame-width) 8)) (buffer-name eglot--help-buffer)))) (help-mode)))) - (eldoc-echo-area-use-multiline-p - ;; Can't really honour non-t non-nil values if this var - (eldoc-message string)) + ((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 - (eldoc-message (eglot--first-line-of-doc string))))) + ;; 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." From a4c7fdfd2d4b512043899a0b1ce88d84aa231f53 Mon Sep 17 00:00:00 2001 From: Gary Oberbrunner Date: Tue, 26 May 2020 14:13:44 +0100 Subject: [PATCH 513/771] Also consider label of a completionitem for snippets Copyright-paperwork-exempt: yes * eglot.el (eglot-completion-at-point): Consider label when expanding snippets. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/480 --- lisp/progmodes/eglot.el | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cc6029079af..fe8eeee9aea 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2152,9 +2152,7 @@ is not active." :exit-function (lambda (proxy _status) (eglot--dbind ((CompletionItem) insertTextFormat - insertText - textEdit - additionalTextEdits) + insertText textEdit additionalTextEdits label) (funcall resolve-maybe (or (get-text-property 0 'eglot--lsp-item proxy) @@ -2189,7 +2187,7 @@ is not active." ;; whole completion, since `insertText' is the full ;; completion's text. (delete-region (- (point) (length proxy)) (point)) - (funcall snippet-fn insertText)))) + (funcall snippet-fn (or insertText label))))) (eglot--signal-textDocument/didChange) (eglot-eldoc-function))))))) From e14934fef20460716c9e4588749c46e979dc154f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 25 May 2020 11:18:21 +0100 Subject: [PATCH 514/771] Replace uses of project-roots with project-root * eglot.el (Package-Requires): Require project 0.3.0. (eglot--connect, eglot-handle-request) (eglot-initialization-options, eglot--eclipse-jdt-contact): Use project-root. --- lisp/progmodes/eglot.el | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fe8eeee9aea..dc0200d617f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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") (package "0.1.1") (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.0.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 @@ -851,7 +851,7 @@ Each function is passed the server as an argument") (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." - (let* ((default-directory (car (project-roots project))) + (let* ((default-directory (project-root project)) (nickname (file-name-base (directory-file-name default-directory))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) autostart-inferior-process @@ -947,7 +947,7 @@ This docstring appeases checkdoc, that's all." (lambda () (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) - (let ((default-directory (car (project-roots project))) + (let ((default-directory (project-root project)) (major-mode managed-major-mode)) (hack-dir-local-variables-non-file-buffer) (run-hook-with-args 'eglot-connect-hook server)) @@ -1781,7 +1781,7 @@ When called interactively, use the currently active server" (if (and (not (string-empty-p uri-path)) (file-directory-p uri-path)) uri-path - (car (project-roots (eglot--project server)))))) + (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) (alist-get section eglot-workspace-configuration @@ -2705,16 +2705,14 @@ documentation. Honour `eglot-put-doc-in-help-buffer', `(:workspaceFolders [,@(cl-delete-duplicates (mapcar #'eglot--path-to-uri - (let* ((roots (project-roots (eglot--project server))) - (root (car roots))) - (append - roots - (mapcar - #'file-name-directory - (append - (file-expand-wildcards (concat root "*/pom.xml")) - (file-expand-wildcards (concat root "*/build.gradle")) - (file-expand-wildcards (concat root "*/.project"))))))) + (let* ((root (project-root (eglot--project server)))) + (cons root + (mapcar + #'file-name-directory + (append + (file-expand-wildcards (concat root "*/pom.xml")) + (file-expand-wildcards (concat root "*/build.gradle")) + (file-expand-wildcards (concat root "*/.project"))))))) :test #'string=)] ,@(if-let ((home (or (getenv "JAVA_HOME") (ignore-errors @@ -2762,7 +2760,7 @@ If INTERACTIVE, prompt user for details." (t "config_linux")))) (project (or (project-current) `(transient . ,default-directory))) (workspace - (expand-file-name (md5 (car (project-roots project))) + (expand-file-name (md5 (project-root project)) (concat user-emacs-directory "eglot-eclipse-jdt-cache")))) (unless jar From 5e0cd484ecd9abc9b1a9e1b14468620b9de9c9cc Mon Sep 17 00:00:00 2001 From: Andrii Kolomoiets Date: Tue, 26 May 2020 23:03:39 +0300 Subject: [PATCH 515/771] Use filter-buffer-substring to get buffer text This way modes used to represent hover info text, such as gfm-view-mode can e.g. filter out invisible text by providing own `filter-buffer-substring-function'. * eglot.el (eglot--format-markup): Use `filter-buffer-substring'. GitHub-reference: close https://github.com/joaotavora/eglot/issues/482 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index dc0200d617f..8bb610bb4ed 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1165,10 +1165,10 @@ Doubles as an indicator of snippet support." (_ major-mode)))))) (with-temp-buffer (setq-local markdown-fontify-code-blocks-natively t) - (insert (string-trim string)) + (insert string) (ignore-errors (delay-mode-hooks (funcall mode))) (font-lock-ensure) - (buffer-string)))) + (string-trim (filter-buffer-substring (point-min) (point-max)))))) (defcustom eglot-ignored-server-capabilites (list) "LSP server capabilities that Eglot could use, but won't. From a56c77183a9b59409701cfd78e38b5a50970857d Mon Sep 17 00:00:00 2001 From: Andrii Kolomoiets Date: Wed, 13 May 2020 10:48:26 +0300 Subject: [PATCH 516/771] Simplify eglot-code-actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If no region is active, ask for code actions at point, even if there are no diagnostics at point. Co-authored-by: João Távora * eglot.el (eglot-code-actions): Simplify. GitHub-reference: close https://github.com/joaotavora/eglot/issues/473 --- lisp/progmodes/eglot.el | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8bb610bb4ed..4b25368d735 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2563,15 +2563,13 @@ documentation. Honour `eglot-put-doc-in-help-buffer', current-prefix-arg)) -(defun eglot-code-actions (&optional beg end) - "Get and offer to execute code actions between BEG and END." +(defun eglot-code-actions (beg &optional end) + "Offer to execute code actions between BEG and END. +Interactively, if a region is active, BEG and END are its bounds, +else BEG is point and END is nil, which results in a request for +code actions at point" (interactive - (let (diags) - (cond ((region-active-p) (list (region-beginning) (region-end))) - ((setq diags (flymake-diagnostics (point))) - (list (cl-reduce #'min (mapcar #'flymake-diagnostic-beg diags)) - (cl-reduce #'max (mapcar #'flymake-diagnostic-end diags)))) - (t (list (point-min) (point-max)))))) + (if (region-active-p) `(,(region-beginning) ,(region-end)) `(,(point) nil))) (unless (eglot--server-capable :codeActionProvider) (eglot--error "Server can't execute code actions!")) (let* ((server (eglot--current-server-or-lose)) From 29dbbcc185471136a8dc874619dbf8f2ffba8fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 31 May 2020 11:49:51 +0100 Subject: [PATCH 517/771] Fix small problems around eglot's help buffer Specifically: - correctly format the message shown to the user about doc being truncated - don't show message if the buffer is showing in some frame's window - correctly name the help buffer switched to with `C-h .'. This is still not ideal: - When the `C-h .' suggestion is shown to the user, typing that keybinding shouldn't result in a new LSP request to fetch probably the same info; - All this functionality belongs in eldoc.el. * eglot.el (eglot-help-at-point): Fix buffer name. (eglot--update-doc): Provide more help. --- lisp/progmodes/eglot.el | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4b25368d735..733b69c395d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2270,12 +2270,12 @@ is not active." (jsonrpc-request (eglot--current-server-or-lose) :textDocument/hover (eglot--TextDocumentPositionParams)) (let ((blurb (and (not (seq-empty-p contents)) - (eglot--hover-info contents range)))) + (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*" - (thing-at-point 'symbol))) + (rename-buffer (format "*eglot-help for %s*" hint)) (with-current-buffer standard-output (insert blurb)) (setq-local nobreak-char-display nil))) (display-local-help))))) @@ -2350,16 +2350,18 @@ documentation. Honour `eglot-put-doc-in-help-buffer', (erase-buffer) (insert string) (goto-char (point-min))) - (if eglot-auto-display-help-buffer - (display-buffer (current-buffer)) - (unless (get-buffer-window (current-buffer)) - ;; This prints two lines. Should it print 1? Or - ;; honour max-mini-window-height? - (eglot--message - "%s\n(...truncated. Full help is in `%s')" - (eglot--truncate-string string 1 (- (frame-width) 8)) - (buffer-name eglot--help-buffer)))) - (help-mode)))) + (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)) From 8afdc3d2d16139c2be5d17588d0741861cb294cc 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 518/771] 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 https://github.com/joaotavora/eglot/issues/494 and also issue https://github.com/joaotavora/eglot/issues/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 https://github.com/joaotavora/eglot/issues/481 and https://github.com/joaotavora/eglot/issues/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 --- lisp/progmodes/eglot.el | 259 +++++++++++++--------------------------- 1 file changed, 80 insertions(+), 179 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 733b69c395d..b94fcc31e15 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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))))) +(defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") -(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) - -(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) From 1d4caae44fc0ad08e126b9ba4b0594ef9c524b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 10 Jul 2020 00:28:19 +0100 Subject: [PATCH 519/771] * eglot.el (package-requires): require flymake 1.0.9 and eldoc 1.2.0 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b94fcc31e15..3e9c9641391 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.1.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.2.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 From 814da26d35db42576f7085bef953f5fe2352baef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 11 Jul 2020 00:41:43 +0800 Subject: [PATCH 520/771] Fix sorting of completion items This fixes a problem pointed out by Yuwei Tian . * eglot.el (eglot-completion-at-point): Fix getting :sortText content of the completion item. GitHub-reference: closes https://github.com/joaotavora/eglot/issues/509 --- lisp/progmodes/eglot.el | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3e9c9641391..87fd9c89025 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2053,12 +2053,15 @@ is not active." ;; Commit logs for this function help understand what's going on. (when-let (completion-capability (eglot--server-capable :completionProvider)) (let* ((server (eglot--current-server-or-lose)) - (sort-completions (lambda (completions) - (sort completions - (lambda (a b) - (string-lessp - (or (get-text-property 0 :sortText a) "") - (or (get-text-property 0 :sortText b) "")))))) + (sort-completions + (lambda (completions) + (cl-sort completions + #'string-lessp + :key (lambda (c) + (or (plist-get + (get-text-property 0 'eglot--lsp-item c) + :sortText) + ""))))) (metadata `(metadata . ((display-sort-function . ,sort-completions)))) resp items (cached-proxies :none) (proxies From 34ecaa4b16c6ec4a12e069e2c51616c667d07d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 13 Jul 2020 23:43:35 +0100 Subject: [PATCH 521/771] Reload eldoc if needed on emacs < 28 ElDoc is preloaded in Emacs, so `require`-ing won't guarantee we are using the latest version from GNU Elpa when we load eglot.el. Use an heuristic to see if we need to `load` it in Emacs < 28. * eglot.el (Package-Requires): Require eldoc 1.5.0 (top): Sometimes load eldoc --- lisp/progmodes/eglot.el | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 87fd9c89025..865ca03dd32 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.2.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.9") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.5.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 @@ -72,6 +72,15 @@ (require 'filenotify) (require 'ert) (require 'array) + +;; ElDoc is preloaded in Emacs, so `require'-ing won't guarantee we are +;; using the latest version from GNU Elpa when we load eglot.el. Use an +;; heuristic to see if we need to `load' it in Emacs < 28. +(if (and (< emacs-major-version 28) + (not (boundp 'eldoc-documentation-strategy))) + (load "eldoc") + (require 'eldoc)) + ;; forward-declare, but don't require (Emacs 28 doesn't seem to care) (defvar markdown-fontify-code-blocks-natively) (defvar company-backends) From ad701795985377401eb007a2eeb5ece65d47b0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 13 Jul 2020 23:59:41 +0100 Subject: [PATCH 522/771] Use a hash-table for storing resolved completions * eglot.el (eglot-completion-at-point): use a hash-table for storing resolved completions. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/510 --- lisp/progmodes/eglot.el | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 865ca03dd32..c0f3143ba14 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2103,20 +2103,20 @@ is not active." (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) items))))) - resolved + (resolved (make-hash-table)) (resolve-maybe ;; Maybe completion/resolve JSON object `lsp-comp' into ;; another JSON object, if at all possible. Otherwise, ;; just return lsp-comp. (lambda (lsp-comp) - (cond (resolved resolved) - ((and (eglot--server-capable :completionProvider - :resolveProvider) - (plist-get lsp-comp :data)) - (setq resolved - (jsonrpc-request server :completionItem/resolve - lsp-comp :cancel-on-input t))) - (t lsp-comp)))) + (or (gethash lsp-comp resolved) + (setf (gethash lsp-comp resolved) + (if (and (eglot--server-capable :completionProvider + :resolveProvider) + (plist-get lsp-comp :data)) + (jsonrpc-request server :completionItem/resolve + lsp-comp :cancel-on-input t) + lsp-comp))))) (bounds (bounds-of-thing-at-point 'symbol))) (list (or (car bounds) (point)) From 836127f39447fb93ffc9f2af2d0b0abbc2b4f824 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Sat, 20 Jun 2020 21:47:04 -0700 Subject: [PATCH 523/771] Ensure completion terminates in correct buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To design a completion-in-region-function replacement that leverages the elements in completion-at-point-functions, we must ensure that their :exit-function parts execute in the correct buffer. That is the buffer where the text to be completed lives, not necessarily the buffer being used for user interaction. Later on, this guarantee should be provided by Emacs itself, perhaps by putting the correct with-current-buffer call in completion--done. Copyright-paperwork-exempt: yes Co-authored-by: João Távora * eglot.el (eglot-completion-at-point): Ensure :exit-function's buffer is where the source is. GitHub-reference: close https://github.com/joaotavora/eglot/issues/505 --- lisp/progmodes/eglot.el | 86 ++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c0f3143ba14..0b233377b12 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2175,45 +2175,53 @@ is not active." (line-beginning-position)))) :exit-function (lambda (proxy _status) - (eglot--dbind ((CompletionItem) insertTextFormat - insertText textEdit additionalTextEdits label) - (funcall - resolve-maybe - (or (get-text-property 0 'eglot--lsp-item proxy) - ;; When selecting from the *Completions* - ;; buffer, `proxy' won't have any properties. - ;; A lookup should fix that (github#148) - (get-text-property - 0 'eglot--lsp-item - (cl-find proxy (funcall proxies) :test #'string=)))) - (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (cond (textEdit - ;; Undo (yes, undo) the newly inserted completion. - ;; If before completion the buffer was "foo.b" and - ;; now is "foo.bar", `proxy' will be "bar". We - ;; want to delete only "ar" (`proxy' minus the - ;; symbol whose bounds we've calculated before) - ;; (github#160). - (delete-region (+ (- (point) (length proxy)) - (if bounds (- (cdr bounds) (car bounds)) 0)) - (point)) - (eglot--dbind ((TextEdit) range newText) textEdit - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (delete-region beg end) - (goto-char beg) - (funcall (or snippet-fn #'insert) newText))) - (when (cl-plusp (length additionalTextEdits)) - (eglot--apply-text-edits additionalTextEdits))) - (snippet-fn - ;; A snippet should be inserted, but using plain - ;; `insertText'. This requires us to delete the - ;; whole completion, since `insertText' is the full - ;; completion's text. - (delete-region (- (point) (length proxy)) (point)) - (funcall snippet-fn (or insertText label))))) - (eglot--signal-textDocument/didChange) - (eldoc))))))) + ;; To assist in using this whole `completion-at-point' + ;; function inside `completion-in-region', ensure the exit + ;; function runs in the buffer where the completion was + ;; triggered from. This should probably be in Emacs itself. + ;; (github#505) + (with-current-buffer (if (minibufferp) + (window-buffer (minibuffer-selected-window)) + (current-buffer)) + (eglot--dbind ((CompletionItem) insertTextFormat + insertText textEdit additionalTextEdits label) + (funcall + resolve-maybe + (or (get-text-property 0 'eglot--lsp-item proxy) + ;; When selecting from the *Completions* + ;; buffer, `proxy' won't have any properties. + ;; A lookup should fix that (github#148) + (get-text-property + 0 'eglot--lsp-item + (cl-find proxy (funcall proxies) :test #'string=)))) + (let ((snippet-fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (cond (textEdit + ;; Undo (yes, undo) the newly inserted completion. + ;; If before completion the buffer was "foo.b" and + ;; now is "foo.bar", `proxy' will be "bar". We + ;; want to delete only "ar" (`proxy' minus the + ;; symbol whose bounds we've calculated before) + ;; (github#160). + (delete-region (+ (- (point) (length proxy)) + (if bounds (- (cdr bounds) (car bounds)) 0)) + (point)) + (eglot--dbind ((TextEdit) range newText) textEdit + (pcase-let ((`(,beg . ,end) (eglot--range-region range))) + (delete-region beg end) + (goto-char beg) + (funcall (or snippet-fn #'insert) newText))) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) + (snippet-fn + ;; A snippet should be inserted, but using plain + ;; `insertText'. This requires us to delete the + ;; whole completion, since `insertText' is the full + ;; completion's text. + (delete-region (- (point) (length proxy)) (point)) + (funcall snippet-fn (or insertText label))))) + (eglot--signal-textDocument/didChange) + (eldoc)))))))) (defun eglot--hover-info (contents &optional range) (let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) From 9511280768fb6239f88fc3eaa7398c972fa9dd00 Mon Sep 17 00:00:00 2001 From: Steven vanZyl Date: Fri, 14 Aug 2020 10:44:38 -0400 Subject: [PATCH 524/771] Add built-int support for godot engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copyright-paperwork-exempt: yes Co-authored-by: João Távora * README.md: mention Godot * eglot.el (eglot-server-programs): Add godot engine via port GitHub-reference: close https://github.com/joaotavora/eglot/issues/511 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0b233377b12..023c0dfe7ab 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -121,7 +121,8 @@ language-server/bin/php-language-server.php")) (scala-mode . ("metals-emacs")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) - (erlang-mode . ("erlang_ls" "--transport" "stdio"))) + (erlang-mode . ("erlang_ls" "--transport" "stdio")) + (gdscript-mode . ("localhost" 6008)) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From 07c8219fa24f15213b1e1899a6824f1146954ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 16 Aug 2020 19:10:23 +0100 Subject: [PATCH 525/771] Correct paren mismatch blunder introduced by earlier commit Per https://github.com/joaotavora/eglot/issues/512. * eglot.el (eglot-server-programs): properly close parenthesis. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/521 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 023c0dfe7ab..a4ba1f9f934 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -122,7 +122,7 @@ language-server/bin/php-language-server.php")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) - (gdscript-mode . ("localhost" 6008)) + (gdscript-mode . ("localhost" 6008))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From c37c30f134e03d0d982aef714b94bda56fe8d860 Mon Sep 17 00:00:00 2001 From: "Paul M. Rodriguez" Date: Fri, 21 Aug 2020 17:27:40 -0500 Subject: [PATCH 526/771] Provide suitable default to m-x eglot-rename Copyright-paperwork-exempt: Yes * eglot (eglot-rename): Provide a default value. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/524 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a4ba1f9f934..64543d7bb87 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2477,7 +2477,9 @@ is not active." (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." (interactive - (list (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point))))) + (list (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point)) + nil nil nil nil + (symbol-name (symbol-at-point))))) (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit From 9ca5b69dc6a832d64a09e777b12fe320821d6c7f Mon Sep 17 00:00:00 2001 From: R Primus Date: Sun, 30 Aug 2020 13:54:14 +0100 Subject: [PATCH 527/771] Unbreak haskell's hie-wrapper built-in incantation * eglot.el (eglot-server-programs): Add required argument for hie-wrapper Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/528 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 64543d7bb87..c752322b58a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -100,7 +100,7 @@ typescript-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) - (php-mode . ("php" "vendor/felixfbecker/\ + (php-mode . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) @@ -108,7 +108,7 @@ language-server/bin/php-language-server.php")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) - (haskell-mode . ("hie-wrapper")) + (haskell-mode . ("hie-wrapper" "--lsp")) (elm-mode . ("elm-language-server")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) From 7d506b0cd5a9afe68b66d6553520074d9c06c8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 1 Sep 2020 19:10:55 +0100 Subject: [PATCH 528/771] Don't send json null (elisp nil) down the wire * eglot.el (eglot-initialization-options) (eglot-client-capabilities): Use eglot--{}, not nil. GitHub-reference: per https://github.com/joaotavora/eglot/issues/300 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c752322b58a..60bc56e8aa6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -508,7 +508,7 @@ treated as in `eglot-dbind'." (cl-defgeneric eglot-initialization-options (server) "JSON object to send under `initializationOptions'" - (:method (_s) nil)) ; blank default + (:method (_s) eglot--{})) ; blank default (cl-defgeneric eglot-register-capability (server method id &rest params) "Ask SERVER to register capability METHOD marked with ID." @@ -581,7 +581,7 @@ treated as in `eglot-dbind'." :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) :publishDiagnostics `(:relatedInformation :json-false)) - :experimental (list)))) + :experimental eglot--{}))) (defclass eglot-lsp-server (jsonrpc-process-connection) ((project-nickname From e4f4762e7acd7ee235aada8c51177a08502f5c85 Mon Sep 17 00:00:00 2001 From: Damien Merenne Date: Wed, 28 Oct 2020 21:40:32 +0100 Subject: [PATCH 529/771] Handle lsp 3.15's ispreferred code action property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot--lsp-interface-alist): Add :isPreferred to CodeAction. (eglot-client-capabilities): Announce :isPreferredSupport. (eglot-code-actions): Consider preferred CodeAction item. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/558 --- lisp/progmodes/eglot.el | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 60bc56e8aa6..bd3bb5337ab 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -244,7 +244,7 @@ let the buffer grow forever." (eval-and-compile (defvar eglot--lsp-interface-alist `( - (CodeAction (:title) (:kind :diagnostics :edit :command)) + (CodeAction (:title) (:kind :diagnostics :edit :command :isPreferred)) (ConfigurationItem () (:scopeUri :section)) (Command ((:title . string) (:command . string)) (:arguments)) (CompletionItem (:label) @@ -576,7 +576,8 @@ treated as in `eglot-dbind'." ["quickfix" "refactor" "refactor.extract" "refactor.inline" "refactor.rewrite" - "source" "source.organizeImports"]))) + "source" "source.organizeImports"])) + :isPreferredSupport t) :formatting `(:dynamicRegistration :json-false) :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) @@ -2516,12 +2517,19 @@ code actions at point" (cons title all)) actions) (eglot--error "No code actions here"))) + (preferred-action (cl-find-if + (jsonrpc-lambda (&key isPreferred &allow-other-keys) + isPreferred) + actions)) (menu `("Eglot code actions:" ("dummy" ,@menu-items))) (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) (cdr (assoc (completing-read "[eglot] Pick an action: " menu-items nil t - nil nil (car menu-items)) + nil nil (or (plist-get + preferred-action + :title) + (car menu-items))) menu-items))))) (eglot--dcase action (((Command) command arguments) From 5646b874b2f340cd159429cd141df9f68e0e12be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 3 Nov 2020 10:26:03 +0000 Subject: [PATCH 530/771] Don't force eglot-strict-mode completely in eglot--dcase Doing so was by design, since there's much ambiguity between the CodeAction and Command objects. But 'disallow-non-standard-keys is not necessary to disambiguate, and proved harmful in this bug. * eglot.el (eglot--dcase): Don't disallow (eglot--check-dspec): Fix docstring. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/558 --- lisp/progmodes/eglot.el | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bd3bb5337ab..b0bd213b48d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -373,7 +373,7 @@ on unknown notifications and errors on unknown requests. :optional-keys (mapcar #'car optional)))) (defun eglot--check-dspec (interface-name dspec) - "Check if variables in DSPEC " + "Check destructuring spec DSPEC against INTERFACE-NAME." (cl-destructuring-bind (&key required-keys optional-keys &allow-other-keys) (eglot--interface interface-name) (cond ((or required-keys optional-keys) @@ -457,10 +457,14 @@ treated as in `eglot-dbind'." (cond (interface-name (eglot--check-dspec interface-name vars) ;; In this mode, in runtime, we assume - ;; `eglot-strict-mode' is fully on, otherwise we + ;; `eglot-strict-mode' is partially on, otherwise we ;; can't disambiguate between certain types. `(ignore-errors - (eglot--check-object ',interface-name ,obj-once))) + (eglot--check-object + ',interface-name ,obj-once + t + (memq 'disallow-non-standard-keys eglot-strict-mode) + t))) (t ;; In this interface-less mode we don't check ;; `eglot-strict-mode' at all: just check that the object From e609841f6f987698e41e362b397c0130fa4103e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 14 Dec 2020 17:08:26 +0000 Subject: [PATCH 531/771] Don't let m-x fill-paragraph break didchange M-x fill-paragraph represents some paragraph-fillling changes very summarily. Filling 1 // foo 2 bar Into 1 // foo bar Only makes two changes: a deletion of the "// " and a replacement of a newline with a space character. The second change fooled Eglot's fix for https://github.com/joaotavora/eglot/issues/259, by making a change similar to the one it is made to detect and correct. That fix should taget things that happen on the same line, this not being one of those things. * eglot.el (eglot--after-change): Only apply fix to https://github.com/joaotavora/eglot/issues/259 if case-fiddling happens on same line. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/367 --- lisp/progmodes/eglot.el | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b0bd213b48d..aa89ae98c67 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1745,10 +1745,15 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." ;; github#259: With `upcase-word' or somesuch, ;; `before-change-functions' always records the whole word's ;; `beg' and `end'. Not only is this longer than needed but - ;; conflicts with the args received here. Detect this using - ;; markers recorded earlier and `pre-change-len', then fix it. + ;; conflicts with the args received here, which encompass just + ;; the parts of the word that changed (if any). We detect this + ;; using markers recorded earlier and at looking + ;; `pre-change-len'. We also ensure that the before bounds + ;; indeed belong to the same line (if we don't, we get could get + ;; #367). (when (and (= b-end b-end-marker) (= b-beg b-beg-marker) - (not (zerop pre-change-length))) + (not (zerop pre-change-length)) + (= (plist-get lsp-beg :line) (plist-get lsp-end :line))) (setq lsp-end (eglot--pos-to-lsp-position end) lsp-beg (eglot--pos-to-lsp-position beg))) (setcar eglot--recent-changes From 73b1707c411b57f217b21da9ac68cf063b4e963e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 15 Dec 2020 12:24:13 +0000 Subject: [PATCH 532/771] Robustify previous fix of onchange breakage From the in-code comments: ;; githubhttps://github.com/joaotavora/eglot/issues/259 and githubhttps://github.com/joaotavora/eglot/issues/367: With `capitalize-word' or somesuch, ;; `before-change-functions' always records the whole word's `b-beg' ;; and `b-end'. Similarly, when coalescing two lines into one, ;; `fill-paragraph' they mark the end of the first line up to the end ;; of the second line. In both situations, args received here ;; contradict that information: `beg' and `end' will differ by 1 and ;; will likely only encompass the letter that was capitalized or, in ;; the sentence-joining situation, the replacement of the newline with ;; a space. That's we keep markers _and_ positions so we're able to ;; detect and correct this. We ignore `beg', `len' and ;; `pre-change-len' and send "fuller" information about the region ;; from the markers. I've also experimented with doing this ;; unconditionally but it seems to break when newlines are added. * eglot.el (eglot--after-change): Robustify fix. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/367 --- lisp/progmodes/eglot.el | 42 +++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index aa89ae98c67..3ebdd04d9f7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1729,8 +1729,8 @@ THINGS are either registrations or unregisterations (sic)." ;; (github#259) (push `(,(eglot--pos-to-lsp-position beg) ,(eglot--pos-to-lsp-position end) - (,beg . ,(copy-marker beg)) - (,end . ,(copy-marker end))) + (,beg . ,(copy-marker beg nil)) + (,end . ,(copy-marker end t))) eglot--recent-changes))) (defun eglot--after-change (beg end pre-change-length) @@ -1742,23 +1742,29 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." (`(,lsp-beg ,lsp-end (,b-beg . ,b-beg-marker) (,b-end . ,b-end-marker)) - ;; github#259: With `upcase-word' or somesuch, + ;; github#259 and github#367: With `capitalize-word' or somesuch, ;; `before-change-functions' always records the whole word's - ;; `beg' and `end'. Not only is this longer than needed but - ;; conflicts with the args received here, which encompass just - ;; the parts of the word that changed (if any). We detect this - ;; using markers recorded earlier and at looking - ;; `pre-change-len'. We also ensure that the before bounds - ;; indeed belong to the same line (if we don't, we get could get - ;; #367). - (when (and (= b-end b-end-marker) (= b-beg b-beg-marker) - (not (zerop pre-change-length)) - (= (plist-get lsp-beg :line) (plist-get lsp-end :line))) - (setq lsp-end (eglot--pos-to-lsp-position end) - lsp-beg (eglot--pos-to-lsp-position beg))) - (setcar eglot--recent-changes - `(,lsp-beg ,lsp-end ,pre-change-length - ,(buffer-substring-no-properties beg end)))) + ;; `b-beg' and `b-end'. Similarly, when coalescing two lines + ;; into one, `fill-paragraph' they mark the end of the first line + ;; up to the end of the second line. In both situations, args + ;; received here contradict that information: `beg' and `end' + ;; will differ by 1 and will likely only encompass the letter + ;; that was capitalized or, in the sentence-joining situation, + ;; the replacement of the newline with a space. That's we keep + ;; markers _and_ positions so we're able to detect and correct + ;; this. We ignore `beg', `len' and `pre-change-len' and send + ;; "fuller" information about the region from the markers. I've + ;; also experimented with doing this unconditionally but it seems + ;; to break when newlines are added. + (if (and (= b-end b-end-marker) (= b-beg b-beg-marker) + (or (/= beg b-beg) (/= end b-end))) + (setcar eglot--recent-changes + `(,lsp-beg ,lsp-end ,(- b-end-marker b-beg-marker) + ,(buffer-substring-no-properties b-beg-marker + b-end-marker))) + (setcar eglot--recent-changes + `(,lsp-beg ,lsp-end ,pre-change-length + ,(buffer-substring-no-properties beg end))))) (_ (setf eglot--recent-changes :emacs-messup))) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) (let ((buf (current-buffer))) From d8b863651e2181cbf5c6d9e29e87ff6f7822b227 Mon Sep 17 00:00:00 2001 From: TANIGUCHI Kohei Date: Wed, 16 Dec 2020 19:19:30 +0900 Subject: [PATCH 533/771] Use haskell-language-server in eglot-server-programs Use haskell-language-server instead of deprecated Haskell IDE Engine https://github.com/haskell/haskell-language-server https://github.com/haskell/haskell-ide-engine#deprecated * README.md: Replace Haskell IDE Engine with haskell-language-server * eglot.el (eglot-server-programs): Replace hie-wrapper with haskell-language-server-wrapper Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/572 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3ebdd04d9f7..e148403590a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -108,7 +108,7 @@ language-server/bin/php-language-server.php")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) - (haskell-mode . ("hie-wrapper" "--lsp")) + (haskell-mode . ("haskell-language-server-wrapper" "--lsp")) (elm-mode . ("elm-language-server")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) From d18f546844350d7fd91477dcaf0a65772828b463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Dec 2020 10:22:56 +0000 Subject: [PATCH 534/771] Cosmetic whitespace fix (indentation, long lines, tabs->spaces) * eglot.el (eglot-server-programs, for, eglot--after-change) (eglot-code-actions, eglot-register-capability) (eglot-register-capability): Fix whitespace. --- lisp/progmodes/eglot.el | 43 +++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e148403590a..404ed19049c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -100,15 +100,16 @@ typescript-mode) . ("javascript-typescript-stdio")) (sh-mode . ("bash-language-server" "start")) - (php-mode . ("php" "vendor/felixfbecker/\ + (php-mode + . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) . ("ocaml-language-server" "--stdio")) (ruby-mode - . ("solargraph" "socket" "--port" - :autoport)) - (haskell-mode . ("haskell-language-server-wrapper" "--lsp")) + . ("solargraph" "socket" "--port" :autoport)) + (haskell-mode + . ("haskell-language-server-wrapper" "--lsp")) (elm-mode . ("elm-language-server")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) @@ -1522,7 +1523,7 @@ Uses THING, FACE, DEFS and PREPEND." (priority . ,(+ 50 i)) (keymap . ,(let ((map (make-sparse-keymap))) (define-key map [mouse-1] - (eglot--mouse-call 'eglot-code-actions)) + (eglot--mouse-call 'eglot-code-actions)) map))))) @@ -1763,8 +1764,8 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." ,(buffer-substring-no-properties b-beg-marker b-end-marker))) (setcar eglot--recent-changes - `(,lsp-beg ,lsp-end ,pre-change-length - ,(buffer-substring-no-properties beg end))))) + `(,lsp-beg ,lsp-end ,pre-change-length + ,(buffer-substring-no-properties beg end))))) (_ (setf eglot--recent-changes :emacs-messup))) (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) (let ((buf (current-buffer))) @@ -2533,9 +2534,9 @@ code actions at point" actions) (eglot--error "No code actions here"))) (preferred-action (cl-find-if - (jsonrpc-lambda (&key isPreferred &allow-other-keys) - isPreferred) - actions)) + (jsonrpc-lambda (&key isPreferred &allow-other-keys) + isPreferred) + actions)) (menu `("Eglot code actions:" ("dummy" ,@menu-items))) (action (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event menu) @@ -2572,16 +2573,16 @@ code actions at point" finally return result)) (cl-defmethod eglot-register-capability - (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) + (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" (eglot-unregister-capability server method id) (let* (success (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) globPattern) watchers)) - (glob-dirs - (delete-dups (mapcar #'file-name-directory - (mapcan #'file-expand-wildcards globs))))) + (glob-dirs + (delete-dups (mapcar #'file-name-directory + (mapcan #'file-expand-wildcards globs))))) (cl-labels ((handle-event (event) @@ -2605,13 +2606,13 @@ code actions at point" (handle-event `(,desc 'created ,file1))))))) (unwind-protect (progn - (dolist (dir glob-dirs) - (push (file-notify-add-watch dir '(change) #'handle-event) - (gethash id (eglot--file-watches server)))) - (setq - success - `(:message ,(format "OK, watching %s directories in %s watchers" - (length glob-dirs) (length watchers))))) + (dolist (dir glob-dirs) + (push (file-notify-add-watch dir '(change) #'handle-event) + (gethash id (eglot--file-watches server)))) + (setq + success + `(:message ,(format "OK, watching %s directories in %s watchers" + (length glob-dirs) (length watchers))))) (unless success (eglot-unregister-capability server method id)))))) From ad47072c322224c382f2b4ed05d025fa9217a69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Dec 2020 16:37:31 +0000 Subject: [PATCH 535/771] Allow eglot to stay out of xref configuration * eglot.el (eglot-stay-out-of): Rework docstring. (eglot--managed-mode): Can now stay out of xref. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/569 --- lisp/progmodes/eglot.el | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 404ed19049c..c240c4251ba 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1288,6 +1288,10 @@ and just return it. PROMPT shouldn't end with a question mark." (defvar eglot-stay-out-of '() "List of Emacs things that Eglot should try to stay of. +Each element is a string, a symbol, or a regexp which is matched +against a variable's name. Examples include the string +\"company\" or the symbol `xref'. + Before Eglot starts \"managing\" a particular buffer, it opinionatedly sets some peripheral Emacs facilites, such as Flymake, Xref and Company. These overriding settings help ensure @@ -1296,9 +1300,8 @@ consistent Eglot behaviour and only stay in place until previous settings are restored. However, if you wish for Eglot to stay out of a particular Emacs -facility that you'd like to keep control of, add a string, a -symbol, or a regexp here that will be matched against the -variable's name, and Eglot will refrain from setting it. +facility that you'd like to keep control of add an element to +this list and Eglot will refrain from setting it. For example, to keep your Company customization use @@ -1338,13 +1341,14 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (add-hook 'after-change-functions 'eglot--after-change nil t) (add-hook 'before-change-functions 'eglot--before-change nil t) (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t) - ;; Prepend "didClose" to the hook after the "onoff", so it will run first + ;; Prepend "didClose" to the hook after the "nonoff", so it will run first (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) (add-hook 'after-revert-hook 'eglot--after-revert-hook nil t) (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) - (add-hook 'xref-backend-functions 'eglot-xref-backend nil t) + (unless (eglot--stay-out-of-p 'xref) + (add-hook 'xref-backend-functions 'eglot-xref-backend nil t)) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) (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) From 2a88cffd69e54e759537f3b04d91158843d2ca32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 16 Dec 2020 16:40:41 +0000 Subject: [PATCH 536/771] Bump eglot version to 1.7 * eglot.el (Version): Bump to 1.7. (Package-Requires): Bump dependency versions. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c240c4251ba..35c959bb20d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,12 +2,12 @@ ;; Copyright (C) 2018-2020 Free Software Foundation, Inc. -;; Version: 1.6 +;; Version: 1.7 ;; Author: João Távora ;; 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.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.5.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.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 From e36fe5985153effff3cba7328ae3b6f0bd771147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 22 Dec 2020 17:31:52 +0000 Subject: [PATCH 537/771] Use flex completion if available by default * eglot.el (eglot--managed-mode): Set completion-styles. GitHub-reference: close https://github.com/joaotavora/eglot/issues/575 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 35c959bb20d..1a53c16dfc9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1362,6 +1362,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) + (eglot--setq-saving completion-styles '(flex basic)) (unless (eglot--stay-out-of-p 'imenu) (add-function :before-until (local 'imenu-create-index-function) #'eglot-imenu)) From 7443bcf612c8df48004a9729f3a3514b985913bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 22 Dec 2020 17:35:08 +0000 Subject: [PATCH 538/771] Don't block in eglot-imenu if performing non-essential task eglot-imenu is used by imenu which in turn is used by which-func-mode called from an idle timer. We don't want it to block in that situation. Latest which-func mode now sets "non-essential" when performing its duties, so we leverage that in eglot-imenu. * eglot.el (eglot-imenu): Use non-essential. GitHub-reference: close https://github.com/joaotavora/eglot/issues/212 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1a53c16dfc9..c7f70ea141e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2415,7 +2415,8 @@ is not active." (jsonrpc-request (eglot--current-server-or-lose) :textDocument/documentSymbol `(:textDocument - ,(eglot--TextDocumentIdentifier)))))))) + ,(eglot--TextDocumentIdentifier)) + :cancel-on-input non-essential)))))) (defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." From 40453c991e12d966a950dc6ec2fe522e57c5a5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20H=C3=B6tzel?= Date: Tue, 5 Jan 2021 11:56:06 +0100 Subject: [PATCH 539/771] Flex completion style is not available on emacs < 27 * eglot.el (eglot--managed-mode): check if flex style available Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/582 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c7f70ea141e..9267a138bd3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1362,7 +1362,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend t)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) - (eglot--setq-saving completion-styles '(flex basic)) + (when (assoc 'flex completion-styles-alist) + (eglot--setq-saving completion-styles '(flex basic))) (unless (eglot--stay-out-of-p 'imenu) (add-function :before-until (local 'imenu-create-index-function) #'eglot-imenu)) From 30139cc1f482c925861c54e9a6ea06effab02ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 10 Jan 2021 16:42:59 +0000 Subject: [PATCH 540/771] C-u m-x eglot discards class guessed by eglot--guess-contact This will prevent C-u M-x eglot RET rust-analyzer from bringing into play the eglot-rls server class, which is only valid for the default RLS server. Found when testing for https://github.com/joaotavora/eglot/issues/526, though this bug might not necessarily be the problem being reported there. * eglot.el (eglot--guess-contact): Don't assume guessed class if INTERACTIVE. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9267a138bd3..5bd087c4354 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -725,7 +725,8 @@ be guessed." (guess (if (functionp guess) (funcall guess interactive) guess)) - (class (or (and (consp guess) (symbolp (car guess)) + (class (or (and (not interactive) + (consp guess) (symbolp (car guess)) (prog1 (car guess) (setq guess (cdr guess)))) 'eglot-lsp-server)) (program (and (listp guess) From 9622f03b5c679e1d69337c0758507ff3a3928207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 13 Jan 2021 15:13:32 +0000 Subject: [PATCH 541/771] Unbreak interactivee eglot--connect for complex contact specs The previous commit for https://github.com/joaotavora/eglot/issues/526 was completely botched. One has to check current-prefix-arg for the presence of C-u, not eglot--guess-contact INTERACTIVE arg. * eglot.el (eglot--guess-contact): Be more careful when processing guess. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/593 --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5bd087c4354..f843c2dba65 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -725,9 +725,9 @@ be guessed." (guess (if (functionp guess) (funcall guess interactive) guess)) - (class (or (and (not interactive) - (consp guess) (symbolp (car guess)) - (prog1 (car guess) (setq guess (cdr guess)))) + (class (or (and (consp guess) (symbolp (car guess)) + (prog1 (unless current-prefix-arg (car guess)) + (setq guess (cdr guess)))) 'eglot-lsp-server)) (program (and (listp guess) (stringp (car guess)) From 2abd7be6b3e23b6fb9ef424352cdfb54496a1b47 Mon Sep 17 00:00:00 2001 From: Augusto Stoffel Date: Wed, 13 Jan 2021 19:41:10 +0100 Subject: [PATCH 542/771] Define a face for symbol highlight Also per https://github.com/joaotavora/eglot/issues/583. * eglot.el (eglot-highlight-symbol-face): New face. (eglot--highlight-piggyback): Use it. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/584 --- lisp/progmodes/eglot.el | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f843c2dba65..6d51fc74fc2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -166,6 +166,10 @@ of those modes. CONTACT can be: should not ask the user for any input, and return nil or signal an error if it can't produce a valid CONTACT.") +(defface eglot-highlight-symbol-face + '((t (:inherit bold))) + "Face used to highlight the symbol at point.") + (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) "Face for package-name in EGLOT's mode line.") @@ -2367,7 +2371,7 @@ is not active." (pcase-let ((`(,beg . ,end) (eglot--range-region range))) (let ((ov (make-overlay beg end))) - (overlay-put ov 'face 'highlight) + (overlay-put ov 'face 'eglot-highlight-symbol-face) (overlay-put ov 'evaporate t) ov))) highlights)))) From 26b10c6dafd126d347fa1f835dc9a974953e3282 Mon Sep 17 00:00:00 2001 From: Augusto Stoffel Date: Wed, 13 Jan 2021 19:43:08 +0100 Subject: [PATCH 543/771] Use `path-separator', not ":", in eclipse/jdt custom code This is needed on Windows. * eglot.el (eglot--eclipse-jdt-contact): Replace literal ":" by `path-separator'. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/513 --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d51fc74fc2..b8db7f9057f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2693,8 +2693,8 @@ If INTERACTIVE, prompt user for details." "org\\.eclipse\\.equinox\\.launcher_.*\\.jar$" (file-name-nondirectory path)) (file-exists-p path)))) - (let* ((classpath (or (getenv "CLASSPATH") ":")) - (cp-jar (cl-find-if #'is-the-jar (split-string classpath ":"))) + (let* ((classpath (or (getenv "CLASSPATH") path-separator)) + (cp-jar (cl-find-if #'is-the-jar (split-string classpath path-separator))) (jar cp-jar) (dir (cond @@ -2732,7 +2732,7 @@ If INTERACTIVE, prompt user for details." (when (and interactive (not cp-jar) (y-or-n-p (concat "Add path to the server program " "to CLASSPATH environment variable?"))) - (setenv "CLASSPATH" (concat (getenv "CLASSPATH") ":" jar))) + (setenv "CLASSPATH" (concat (getenv "CLASSPATH") path-separator jar))) (unless (file-directory-p workspace) (make-directory workspace t)) (cons 'eglot-eclipse-jdt From 6d731fab9c07cd49f83e49ad3e6588ddb47fe98e Mon Sep 17 00:00:00 2001 From: Andrii Kolomoiets Date: Sat, 16 Jan 2021 13:42:32 +0200 Subject: [PATCH 544/771] Run exit-function only for finished completion Per https://github.com/joaotavora/eglot/issues/594. * eglot.el (eglot-completion-at-point): Respect 'status' argument in completion's exit function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/592 --- lisp/progmodes/eglot.el | 100 +++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b8db7f9057f..0447bc85041 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2203,54 +2203,58 @@ is not active." (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) (line-beginning-position)))) :exit-function - (lambda (proxy _status) - ;; To assist in using this whole `completion-at-point' - ;; function inside `completion-in-region', ensure the exit - ;; function runs in the buffer where the completion was - ;; triggered from. This should probably be in Emacs itself. - ;; (github#505) - (with-current-buffer (if (minibufferp) - (window-buffer (minibuffer-selected-window)) - (current-buffer)) - (eglot--dbind ((CompletionItem) insertTextFormat - insertText textEdit additionalTextEdits label) - (funcall - resolve-maybe - (or (get-text-property 0 'eglot--lsp-item proxy) - ;; When selecting from the *Completions* - ;; buffer, `proxy' won't have any properties. - ;; A lookup should fix that (github#148) - (get-text-property - 0 'eglot--lsp-item - (cl-find proxy (funcall proxies) :test #'string=)))) - (let ((snippet-fn (and (eql insertTextFormat 2) - (eglot--snippet-expansion-fn)))) - (cond (textEdit - ;; Undo (yes, undo) the newly inserted completion. - ;; If before completion the buffer was "foo.b" and - ;; now is "foo.bar", `proxy' will be "bar". We - ;; want to delete only "ar" (`proxy' minus the - ;; symbol whose bounds we've calculated before) - ;; (github#160). - (delete-region (+ (- (point) (length proxy)) - (if bounds (- (cdr bounds) (car bounds)) 0)) - (point)) - (eglot--dbind ((TextEdit) range newText) textEdit - (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (delete-region beg end) - (goto-char beg) - (funcall (or snippet-fn #'insert) newText))) - (when (cl-plusp (length additionalTextEdits)) - (eglot--apply-text-edits additionalTextEdits))) - (snippet-fn - ;; A snippet should be inserted, but using plain - ;; `insertText'. This requires us to delete the - ;; whole completion, since `insertText' is the full - ;; completion's text. - (delete-region (- (point) (length proxy)) (point)) - (funcall snippet-fn (or insertText label))))) - (eglot--signal-textDocument/didChange) - (eldoc)))))))) + (lambda (proxy status) + (when (eq status 'finished) + ;; To assist in using this whole `completion-at-point' + ;; function inside `completion-in-region', ensure the exit + ;; function runs in the buffer where the completion was + ;; triggered from. This should probably be in Emacs itself. + ;; (github#505) + (with-current-buffer (if (minibufferp) + (window-buffer (minibuffer-selected-window)) + (current-buffer)) + (eglot--dbind ((CompletionItem) insertTextFormat + insertText textEdit additionalTextEdits label) + (funcall + resolve-maybe + (or (get-text-property 0 'eglot--lsp-item proxy) + ;; When selecting from the *Completions* + ;; buffer, `proxy' won't have any properties. + ;; A lookup should fix that (github#148) + (get-text-property + 0 'eglot--lsp-item + (cl-find proxy (funcall proxies) :test #'string=)))) + (let ((snippet-fn (and (eql insertTextFormat 2) + (eglot--snippet-expansion-fn)))) + (cond (textEdit + ;; Undo (yes, undo) the newly inserted completion. + ;; If before completion the buffer was "foo.b" and + ;; now is "foo.bar", `proxy' will be "bar". We + ;; want to delete only "ar" (`proxy' minus the + ;; symbol whose bounds we've calculated before) + ;; (github#160). + (delete-region (+ (- (point) (length proxy)) + (if bounds + (- (cdr bounds) (car bounds)) + 0)) + (point)) + (eglot--dbind ((TextEdit) range newText) textEdit + (pcase-let ((`(,beg . ,end) + (eglot--range-region range))) + (delete-region beg end) + (goto-char beg) + (funcall (or snippet-fn #'insert) newText))) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) + (snippet-fn + ;; A snippet should be inserted, but using plain + ;; `insertText'. This requires us to delete the + ;; whole completion, since `insertText' is the full + ;; completion's text. + (delete-region (- (point) (length proxy)) (point)) + (funcall snippet-fn (or insertText label))))) + (eglot--signal-textDocument/didChange) + (eldoc))))))))) (defun eglot--hover-info (contents &optional range) (let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) From 581dfb79bffc25a2ef8879aee4862817f6c21e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Sat, 11 Jan 2020 20:12:26 +0100 Subject: [PATCH 545/771] Fix eglot-completion-at-point for multiple matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test-completion case shouldn't return t when there are multiple matches. Similarly, the try-completion should return t only if the match is exact. See (info "(elisp)Programmed Completion"). * eglot.el (eglot-completion-at-point): Instead of testing memberships, use test-completion and try-completion suggested by (info "(elisp)Programmed Completion"). * eglot-tests.el (non-unique-completions): Add new test. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/365 --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0447bc85041..98fa4d9af6f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2154,10 +2154,10 @@ is not active." (cond ((eq action 'metadata) metadata) ; metadata ((eq action 'lambda) ; test-completion - (member probe (funcall proxies))) + (test-completion probe (funcall proxies))) ((eq (car-safe action) 'boundaries) nil) ; boundaries - ((and (null action) ; try-completion - (member probe (funcall proxies)) t)) + ((null action) ; try-completion + (try-completion probe (funcall proxies))) ((eq action t) ; all-completions (cl-remove-if-not (lambda (proxy) From 8b4896f6d2a2245c5794fa8c9ecca1507648daed Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Wed, 27 Jan 2021 14:43:15 -0800 Subject: [PATCH 546/771] Add rnix-lsp server for nix-mode, community suggestion * eglot.el (eglot-server-programs): Add rnix-lsp * README.md: mention rnix-lsp Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/599 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98fa4d9af6f..a0c5dc52032 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -123,6 +123,7 @@ language-server/bin/php-language-server.php")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) + (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE From ae361b0a49d0db70aedfa481070d88752fe62f35 Mon Sep 17 00:00:00 2001 From: ssnnoo <43703153+ssnnoo@users.noreply.github.com> Date: Wed, 27 Jan 2021 09:10:07 +0000 Subject: [PATCH 547/771] Add fortls for fotran (f90-mode) * eglot.el (eglot-server-programs): Add fortls * README.md: mention fortls Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/603 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a0c5dc52032..8cfe9918d2f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -124,7 +124,8 @@ language-server/bin/php-language-server.php")) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) (nix-mode . ("rnix-lsp")) - (gdscript-mode . ("localhost" 6008))) + (gdscript-mode . ("localhost" 6008)) + (f90-mode . ("fortls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From 49327fb04c173249346edb30ac9a8a7cd8aade53 Mon Sep 17 00:00:00 2001 From: Andrii Kolomoiets Date: Thu, 28 Jan 2021 20:36:11 +0200 Subject: [PATCH 548/771] Offer shortcut commands to commonly invoked code actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See also https://github.com/joaotavora/eglot/issues/598. Make eglot-code-actions accept a new action-kind argument. If there is only one action of that kind, apply it. This allows us to create actions shortcuts like eglot-code-action-organize-imports, etc. * eglot.el (eglot-code-actions): Accept new argument action-kind. (eglot--code-action): New function-defining helper macro. (eglot-code-action-organize-imports) (eglot-code-action-extract) (eglot-code-action-inline) (eglot-code-action-rewrite) (eglot-code-action-quickfix): New commands. * README.md: Mention new feature. * NEWS.md: Mention new feature. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/411 --- lisp/progmodes/eglot.el | 77 ++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8cfe9918d2f..276cd1aeb3e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2523,14 +2523,21 @@ is not active." :newName ,newname)) current-prefix-arg)) +(defun eglot--region-bounds () "Region bounds if active, else point and nil." + (if (use-region-p) `(,(region-beginning) ,(region-end)) `(,(point) nil))) -(defun eglot-code-actions (beg &optional end) - "Offer to execute code actions between BEG and END. -Interactively, if a region is active, BEG and END are its bounds, -else BEG is point and END is nil, which results in a request for -code actions at point" +(defun eglot-code-actions (beg &optional end action-kind) + "Offer to execute actions of ACTION-KIND between BEG and END. +If ACTION-KIND is nil, consider all kinds of actions. +Interactively, default BEG and END to region's bounds else BEG is +point and END is nil, which results in a request for code actions +at point. With prefix argument, prompt for ACTION-KIND." (interactive - (if (region-active-p) `(,(region-beginning) ,(region-end)) `(,(point) nil))) + `(,@(eglot--region-bounds) + ,(and current-prefix-arg + (completing-read "[eglot] Action kind: " + '("quickfix" "refactor.extract" "refactor.inline" + "refactor.rewrite" "source.organizeImports"))))) (unless (eglot--server-capable :codeActionProvider) (eglot--error "Server can't execute code actions!")) (let* ((server (eglot--current-server-or-lose)) @@ -2544,27 +2551,35 @@ code actions at point" :context `(:diagnostics [,@(cl-loop for diag in (flymake-diagnostics beg end) - when (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag))) - collect it)])))) + when (cdr (assoc 'eglot-lsp-diag + (eglot--diag-data diag))) + collect it)] + ,@(when action-kind `(:only ,action-kind)))))) (menu-items - (or (mapcar (jsonrpc-lambda (&rest all &key title &allow-other-keys) - (cons title all)) - actions) - (eglot--error "No code actions here"))) + (or (cl-loop for action across actions + ;; Do filtering ourselves, in case the `:only' + ;; didn't go through. + when (or (not action-kind) + (equal action-kind (plist-get action :kind))) + collect (cons (plist-get action :title) action)) + (apply #'eglot--error + (if action-kind `("No \"%s\" code actions here" ,action-kind) + `("No code actions here"))))) (preferred-action (cl-find-if - (jsonrpc-lambda (&key isPreferred &allow-other-keys) - isPreferred) - actions)) - (menu `("Eglot code actions:" ("dummy" ,@menu-items))) - (action (if (listp last-nonmenu-event) - (x-popup-menu last-nonmenu-event menu) - (cdr (assoc (completing-read "[eglot] Pick an action: " - menu-items nil t - nil nil (or (plist-get - preferred-action - :title) - (car menu-items))) - menu-items))))) + (lambda (menu-item) + (plist-get (cdr menu-item) :isPreferred)) + menu-items)) + (default-action (car (or preferred-action (car menu-items)))) + (action (if (and action-kind (null (cadr menu-items))) + (cdr (car menu-items)) + (if (listp last-nonmenu-event) + (x-popup-menu last-nonmenu-event `("Eglot code actions:" + ("dummy" ,@menu-items))) + (cdr (assoc (completing-read + (format "[eglot] Pick an action (default %s): " + default-action) + menu-items nil t nil nil default-action) + menu-items)))))) (eglot--dcase action (((Command) command arguments) (eglot-execute-command server (intern command) arguments)) @@ -2574,6 +2589,18 @@ code actions at point" (eglot--dbind ((Command) command arguments) command (eglot-execute-command server (intern command) arguments))))))) +(defmacro eglot--code-action (name kind) + "Define NAME to execute KIND code action." + `(defun ,name (beg &optional end) + ,(format "Execute '%s' code actions between BEG and END." kind) + (interactive (eglot--region-bounds)) + (eglot-code-actions beg end ,kind))) + +(eglot--code-action eglot-code-action-organize-imports "source.organizeImports") +(eglot--code-action eglot-code-action-extract "refactor.extract") +(eglot--code-action eglot-code-action-inline "refactor.inline") +(eglot--code-action eglot-code-action-rewrite "refactor.rewrite") +(eglot--code-action eglot-code-action-quickfix "quickfix") ;;; Dynamic registration From 470447e22a2a2625cca10791ee49ef6a11764a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 28 Jan 2021 18:56:22 +0000 Subject: [PATCH 549/771] * eglot.el (eglot): tweak docstring grammar. --- lisp/progmodes/eglot.el | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 276cd1aeb3e..b5f6e6f73a1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -780,13 +780,13 @@ be guessed." (defun eglot (managed-major-mode project class contact &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. -The LSP server of CLASS started (or contacted) via CONTACT. If -this operation is successful, current *and future* file buffers -of MANAGED-MAJOR-MODE inside PROJECT automatically become -\"managed\" by the LSP server, meaning information about their -contents is exchanged periodically to provide enhanced -code-analysis via `xref-find-definitions', `flymake-mode', -`eldoc-mode', `completion-at-point', among others. +The LSP server of CLASS is started (or contacted) via CONTACT. +If this operation is successful, current *and future* file +buffers of MANAGED-MAJOR-MODE inside PROJECT become \"managed\" +by the LSP server, meaning information about their contents is +exchanged periodically to provide enhanced code-analysis via +`xref-find-definitions', `flymake-mode', `eldoc-mode', +`completion-at-point', among others. Interactively, the command attempts to guess MANAGED-MAJOR-MODE from current buffer, CLASS and CONTACT from @@ -798,7 +798,7 @@ MANAGED-MAJOR-MODE. PROJECT is a project instance as returned by `project-current'. -CLASS is a subclass of symbol `eglot-lsp-server'. +CLASS is a subclass of `eglot-lsp-server'. CONTACT specifies how to contact the server. It is a keyword-value plist used to initialize CLASS or a plain list as From 93eb72de229a2774198364be7acb7131a71222cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 29 Jan 2021 16:27:55 +0000 Subject: [PATCH 550/771] #fix 608: fix bug in eglot-code-actions Suggested by GitHub user "vconcat". * eglot.el (eglot-code-actions): Use a vector for transmitting action-kind. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/606 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b5f6e6f73a1..98e0bcf14fa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2554,7 +2554,7 @@ at point. With prefix argument, prompt for ACTION-KIND." when (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag))) collect it)] - ,@(when action-kind `(:only ,action-kind)))))) + ,@(when action-kind `(:only [,action-kind])))))) (menu-items (or (cl-loop for action across actions ;; Do filtering ourselves, in case the `:only' From c266aa6b360a683f50b3370942fc7ebfcafc747a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 30 Jan 2021 18:01:19 +0000 Subject: [PATCH 551/771] Flush pending changes to server before code actions request Otherwise the actions returned by the server might be stale when the user selects them. * eglot.el (eglot-code-actions): Issue jsonrpc-request with deferred=t. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/609 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98e0bcf14fa..8403e5dfdb1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2554,7 +2554,8 @@ at point. With prefix argument, prompt for ACTION-KIND." when (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag))) collect it)] - ,@(when action-kind `(:only [,action-kind])))))) + ,@(when action-kind `(:only [,action-kind])))) + :deferred t)) (menu-items (or (cl-loop for action across actions ;; Do filtering ourselves, in case the `:only' From 89fccba0088f765ba6a4d02b7ca4bf53633b43be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 31 Jan 2021 18:18:02 +0000 Subject: [PATCH 552/771] Fully handle lsp glob syntax Thanks to Brian Leung and Dan Peterson for testing and helping me spot bugs. * eglot-tests.el (eglot--glob-match): New test. * eglot.el (eglot--wildcard-to-regexp): Delete. (eglot-register-capability): Rework. (eglot--glob-parse, eglot--glob-compile, eglot--glob-emit-self) (eglot--glob-emit-**, eglot--glob-emit-*, eglot--glob-emit-?) (eglot--glob-emit-{}, eglot--glob-emit-range) (eglot--directories-recursively): New helpers. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/602 --- lisp/progmodes/eglot.el | 113 +++++++++++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 24 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8403e5dfdb1..51ed1c49a96 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2606,40 +2606,32 @@ at point. With prefix argument, prompt for ACTION-KIND." ;;; Dynamic registration ;;; -(defun eglot--wildcard-to-regexp (wildcard) - "(Very lame attempt to) convert WILDCARD to a Elisp regexp." - (cl-loop - with substs = '(("{" . "\\\\(") - ("}" . "\\\\)") - ("," . "\\\\|")) - with string = (wildcard-to-regexp wildcard) - for (pattern . rep) in substs - for target = string then result - for result = (replace-regexp-in-string pattern rep target) - finally return result)) - (cl-defmethod eglot-register-capability (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) "Handle dynamic registration of workspace/didChangeWatchedFiles" (eglot-unregister-capability server method id) (let* (success - (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) - globPattern) - watchers)) - (glob-dirs - (delete-dups (mapcar #'file-name-directory - (mapcan #'file-expand-wildcards globs))))) + (globs (mapcar + (eglot--lambda ((FileSystemWatcher) globPattern) + (cons + (eglot--glob-compile globPattern t t) + (eglot--glob-compile + (replace-regexp-in-string "/[^/]*$" "/" globPattern) t t))) + watchers)) + (dirs-to-watch + (cl-loop for dir in (eglot--directories-recursively) + when (cl-loop for g in globs + thereis (ignore-errors (funcall (cdr g) dir))) + collect dir))) (cl-labels ((handle-event (event) (pcase-let ((`(,desc ,action ,file ,file1) event)) (cond ((and (memq action '(created changed deleted)) - (cl-find file globs + (cl-find file (mapcar #'car globs) :test (lambda (f glob) - (string-match (eglot--wildcard-to-regexp - (expand-file-name glob)) - f)))) + (funcall glob f)))) (jsonrpc-notify server :workspace/didChangeWatchedFiles `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) @@ -2652,13 +2644,13 @@ at point. With prefix argument, prompt for ACTION-KIND." (handle-event `(,desc 'created ,file1))))))) (unwind-protect (progn - (dolist (dir glob-dirs) + (dolist (dir dirs-to-watch) (push (file-notify-add-watch dir '(change) #'handle-event) (gethash id (eglot--file-watches server)))) (setq success `(:message ,(format "OK, watching %s directories in %s watchers" - (length glob-dirs) (length watchers))))) + (length dirs-to-watch) (length watchers))))) (unless success (eglot-unregister-capability server method id)))))) @@ -2669,6 +2661,79 @@ at point. With prefix argument, prompt for ACTION-KIND." (remhash id (eglot--file-watches server)) (list t "OK")) + +;;; Glob heroics +;;; +(defun eglot--glob-parse (glob) + "Compute list of (STATE-SYM EMITTER-FN PATTERN)." + (with-temp-buffer + (save-excursion (insert glob)) + (cl-loop + with grammar = '((:** "\\*\\*/?" eglot--glob-emit-**) + (:* "\\*" eglot--glob-emit-*) + (:? "\\?" eglot--glob-emit-?) + (:/ "/" eglot--glob-emit-self) + (:{} "{[^][/*{}]+}" eglot--glob-emit-{}) + (:range "\\[\\^?[^][/,*{}]+\\]" eglot--glob-emit-range) + (:literal "[^][/,*?{}]+" eglot--glob-emit-self)) + until (eobp) + collect (cl-loop + for (_token regexp emitter) in grammar + thereis (and (re-search-forward (concat "\\=" regexp) nil t) + (list (cl-gensym "state-") emitter (match-string 0))) + finally (error "Glob '%s' invalid at %s" (buffer-string) (point)))))) + +(defun eglot--glob-compile (glob &optional byte-compile noerror) + "Convert GLOB into Elisp function. Maybe BYTE-COMPILE it. +If NOERROR, return predicate, else erroring function." + (let* ((states (eglot--glob-parse glob)) + (body `(with-temp-buffer + (save-excursion (insert string)) + (cl-labels ,(cl-loop for (this that) on states + for (self emit text) = this + for next = (or (car that) 'eobp) + collect (funcall emit text self next)) + (or (,(caar states)) + (error "Glob done but more unmatched text: '%s'" + (buffer-substring (point) (point-max))))))) + (form `(lambda (string) ,(if noerror `(ignore-errors ,body) body)))) + (if byte-compile (byte-compile form) form))) + +(defun eglot--glob-emit-self (text self next) + `(,self () (re-search-forward ,(concat "\\=" (regexp-quote text))) (,next))) + +(defun eglot--glob-emit-** (_ self next) + `(,self () (or (ignore-errors (save-excursion (,next))) + (and (re-search-forward "\\=/?[^/]+/?") (,self))))) + +(defun eglot--glob-emit-* (_ self next) + `(,self () (re-search-forward "\\=[^/]") + (or (ignore-errors (save-excursion (,next))) (,self)))) + +(defun eglot--glob-emit-? (_ self next) + `(,self () (re-search-forward "\\=[^/]") (,next))) + +(defun eglot--glob-emit-{} (arg self next) + (let ((alternatives (split-string (substring arg 1 (1- (length arg))) ","))) + `(,self () + (or ,@(cl-loop for alt in alternatives + collect `(re-search-forward ,(concat "\\=" alt) nil t)) + (error "Failed matching any of %s" ',alternatives)) + (,next)))) + +(defun eglot--glob-emit-range (arg self next) + (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) + `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) + +(defun eglot--directories-recursively (&optional dir) + "Because `directory-files-recursively' isn't complete in 26.3." + (cons (setq dir (expand-file-name (or dir default-directory))) + (cl-loop + with default-directory = dir + with completion-regexp-list = '("^[^.]") + for f in (file-name-all-completions "" dir) + when (file-directory-p f) append (eglot--directories-recursively f)))) + ;;; Rust-specific ;;; From bdf57d5d4e888a6a7b4066b87497da8a8d9e36de Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Sun, 31 Jan 2021 17:28:49 -0800 Subject: [PATCH 553/771] Support activeparameter property for signatureinformation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SignatureInformation.activeParameter is new in version 3.16.0 of the protocol. When non-nil, it is used in place of SignatureHelp.activeParameter. The latter was deemed insufficient in languages where multiple signatures for the same function may exist with arbitrary order of parameters, like Python. Co-authored-by: João Távora * eglot.el (eglot--lsp-interface-alist): Add SignatureInformation.activeParameter. * eglot.el (eglot--sig-info): Prioritize SignatureInformation.activeParameter over SignatureHelp.activeParameter. GitHub-reference: close https://github.com/joaotavora/eglot/issues/605 --- lisp/progmodes/eglot.el | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 51ed1c49a96..27094a2bd50 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -276,7 +276,7 @@ let the buffer grow forever." (ShowMessageParams (:type :message)) (ShowMessageRequestParams (:type :message) (:actions)) (SignatureHelp (:signatures) (:activeSignature :activeParameter)) - (SignatureInformation (:label) (:documentation :parameters)) + (SignatureInformation (:label) (:documentation :parameters :activeParameter)) (SymbolInformation (:name :kind :location) (:deprecated :containerName)) (DocumentSymbol (:name :range :selectionRange :kind) @@ -2265,14 +2265,15 @@ is not active." (if (vectorp contents) contents (list contents)) "\n"))) (when (or heading (cl-plusp (length body))) (concat heading body)))) -(defun eglot--sig-info (sigs active-sig active-param) +(defun eglot--sig-info (sigs active-sig sig-help-active-param) (cl-loop for (sig . moresigs) on (append sigs nil) for i from 0 concat - (eglot--dbind ((SignatureInformation) label documentation parameters) sig + (eglot--dbind ((SignatureInformation) label documentation parameters activeParameter) sig (with-temp-buffer (save-excursion (insert label)) - (let (params-start params-end) + (let ((active-param (or activeParameter sig-help-active-param)) + params-start params-end) ;; Ad-hoc attempt to parse label as () (when (looking-at "\\([^(]+\\)(\\([^)]+\\))") (setq params-start (match-beginning 2) params-end (match-end 2)) From c758ba1a4c38f3edb9779082a7ac808e59774848 Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Mon, 1 Feb 2021 02:44:38 -0800 Subject: [PATCH 554/771] Explicitly require seq.el `seq-empty-p' is not autoloaded in Emacs >= 26.3, so it must be explicitly required. * eglot.el: Require seq.el. GitHub-reference: close https://github.com/joaotavora/eglot/issues/613 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 27094a2bd50..f3b006da1ae 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -59,6 +59,7 @@ (require 'imenu) (require 'cl-lib) (require 'project) +(require 'seq) (require 'url-parse) (require 'url-util) (require 'pcase) From 60724b8c522ff2ef30429170223c1595c47a4d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 1 Feb 2021 14:03:23 +0000 Subject: [PATCH 555/771] Also override global flymake-diagnostic-functions The global value of the flymake-diagnostic-functions is likely to be of little use in Eglot-managed buffers, so don't run it. Likely the value flymake-proc-legacy-flymake is there which is not only likely of little uses but also causes trouble in some situations. The user can easily avert this by leveraging the variable eglot-stay-out-of. * eglot.el (eglot--managed-mode): Don't run global flymake-diagnostic-functions. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/616 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f3b006da1ae..3ea8a2fa7e1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1367,7 +1367,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (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 flymake-diagnostic-functions '(eglot-flymake-backend)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) (when (assoc 'flex completion-styles-alist) From 5e3fa130baccc66e551d62b9b3daab848bcbc6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 1 Feb 2021 15:39:15 +0000 Subject: [PATCH 556/771] Prefer typescript-language-server for js&ts * README.md (Connecting to a server): Prefer typescript-language-server. * eglot.el (eglot-server-programs): Use typescript-language-server. GitHub-reference: close https://github.com/joaotavora/eglot/issues/566 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3ea8a2fa7e1..158a1084993 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -99,7 +99,7 @@ (python-mode . ("pyls")) ((js-mode typescript-mode) - . ("javascript-typescript-stdio")) + . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) (php-mode . ("php" "vendor/felixfbecker/\ From d64ea753f97e96093c148d72b295c6beb1a6443f Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Mon, 1 Feb 2021 07:44:52 -0800 Subject: [PATCH 557/771] Remove duplicate entry for "registration" lsp type * eglot.el (eglot--lsp-interface-alist): Remove extra Registration entry. Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/612 --- lisp/progmodes/eglot.el | 1 - 1 file changed, 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 158a1084993..e1387235ff5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -272,7 +272,6 @@ let the buffer grow forever." (Position (:line :character)) (Range (:start :end)) (Registration (:id :method) (:registerOptions)) - (Registration (:id :method) (:registerOptions)) (ResponseError (:code :message) (:data)) (ShowMessageParams (:type :message)) (ShowMessageRequestParams (:type :message) (:actions)) From 176a6df74e0bbf3e360f2af72c02a70211fd5431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Mon, 1 Feb 2021 17:02:58 +0100 Subject: [PATCH 558/771] Support phps-mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit phps-mode is available from ELPA, php-mode isn't. * eglot.el (eglot-server-programs): Recognize phps-mode as a PHP mode. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/418 --- lisp/progmodes/eglot.el | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e1387235ff5..44648ae41b4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -97,11 +97,10 @@ (defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) (python-mode . ("pyls")) - ((js-mode - typescript-mode) + ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) - (php-mode + ((php-mode phps-mode) . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) From aa4e58409c6c7a394a9a1292b1f5e34d5377323d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 1 Feb 2021 17:23:07 +0000 Subject: [PATCH 559/771] Speed up glob matching 2x with-temp-buffer was taking a lot of time, presumably because it kills the buffer. Since emacs is single-threaded, we can safely reuse a single buffer. * eglot.el (eglot--glob-parse): Simplify grammar. (eglot--glob-compile): Don't with-temp-buffer. GitHub-reference: per https://github.com/joaotavora/eglot/issues/602 --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 44648ae41b4..50fb695319a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2672,10 +2672,9 @@ at point. With prefix argument, prompt for ACTION-KIND." with grammar = '((:** "\\*\\*/?" eglot--glob-emit-**) (:* "\\*" eglot--glob-emit-*) (:? "\\?" eglot--glob-emit-?) - (:/ "/" eglot--glob-emit-self) (:{} "{[^][/*{}]+}" eglot--glob-emit-{}) (:range "\\[\\^?[^][/,*{}]+\\]" eglot--glob-emit-range) - (:literal "[^][/,*?{}]+" eglot--glob-emit-self)) + (:literal "[^][,*?{}]+" eglot--glob-emit-self)) until (eobp) collect (cl-loop for (_token regexp emitter) in grammar @@ -2687,7 +2686,8 @@ at point. With prefix argument, prompt for ACTION-KIND." "Convert GLOB into Elisp function. Maybe BYTE-COMPILE it. If NOERROR, return predicate, else erroring function." (let* ((states (eglot--glob-parse glob)) - (body `(with-temp-buffer + (body `(with-current-buffer (get-buffer-create " *eglot-glob-matcher*") + (erase-buffer) (save-excursion (insert string)) (cl-labels ,(cl-loop for (this that) on states for (self emit text) = this From c453d8df3603a7bacad30647778e92e960ead18b Mon Sep 17 00:00:00 2001 From: Philip Kaludercic Date: Mon, 1 Feb 2021 18:20:37 +0000 Subject: [PATCH 560/771] Make eglot-ignored-server-capabilites defcustom a set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Távora * eglot.el (eglot-ignored-server-capabilites): Now a set. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/467 --- lisp/progmodes/eglot.el | 47 ++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 50fb695319a..9944b18a2e5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1205,30 +1205,29 @@ Doubles as an indicator of snippet support." You could add, for instance, the symbol `:documentHighlightProvider' to prevent automatic highlighting under cursor." - :type '(repeat - (choice - (const :tag "Documentation on hover" :hoverProvider) - (const :tag "Code completion" :completionProvider) - (const :tag "Function signature help" :signatureHelpProvider) - (const :tag "Go to definition" :definitionProvider) - (const :tag "Go to type definition" :typeDefinitionProvider) - (const :tag "Go to implementation" :implementationProvider) - (const :tag "Go to declaration" :implementationProvider) - (const :tag "Find references" :referencesProvider) - (const :tag "Highlight symbols automatically" :documentHighlightProvider) - (const :tag "List symbols in buffer" :documentSymbolProvider) - (const :tag "List symbols in workspace" :workspaceSymbolProvider) - (const :tag "Execute code actions" :codeActionProvider) - (const :tag "Code lens" :codeLensProvider) - (const :tag "Format buffer" :documentFormattingProvider) - (const :tag "Format portion of buffer" :documentRangeFormattingProvider) - (const :tag "On-type formatting" :documentOnTypeFormattingProvider) - (const :tag "Rename symbol" :renameProvider) - (const :tag "Highlight links in document" :documentLinkProvider) - (const :tag "Decorate color references" :colorProvider) - (const :tag "Fold regions of buffer" :foldingRangeProvider) - (const :tag "Execute custom commands" :executeCommandProvider) - (symbol :tag "Other")))) + :type '(set + :tag "Tick the ones you're not interested in" + (const :tag "Documentation on hover" :hoverProvider) + (const :tag "Code completion" :completionProvider) + (const :tag "Function signature help" :signatureHelpProvider) + (const :tag "Go to definition" :definitionProvider) + (const :tag "Go to type definition" :typeDefinitionProvider) + (const :tag "Go to implementation" :implementationProvider) + (const :tag "Go to declaration" :implementationProvider) + (const :tag "Find references" :referencesProvider) + (const :tag "Highlight symbols automatically" :documentHighlightProvider) + (const :tag "List symbols in buffer" :documentSymbolProvider) + (const :tag "List symbols in workspace" :workspaceSymbolProvider) + (const :tag "Execute code actions" :codeActionProvider) + (const :tag "Code lens" :codeLensProvider) + (const :tag "Format buffer" :documentFormattingProvider) + (const :tag "Format portion of buffer" :documentRangeFormattingProvider) + (const :tag "On-type formatting" :documentOnTypeFormattingProvider) + (const :tag "Rename symbol" :renameProvider) + (const :tag "Highlight links in document" :documentLinkProvider) + (const :tag "Decorate color references" :colorProvider) + (const :tag "Fold regions of buffer" :foldingRangeProvider) + (const :tag "Execute custom commands" :executeCommandProvider))) (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." From 14d901c58880f50712585951163bcd7a6567d718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 3 Feb 2021 00:43:35 +0000 Subject: [PATCH 561/771] Tweak glob-parsing grammar Alternative groups {} don't bork on forward slash. * eglot.el (eglot--glob-parse): Tweak {} grammar. GitHub-reference: per https://github.com/joaotavora/eglot/issues/602 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9944b18a2e5..90d973c6b63 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2671,7 +2671,7 @@ at point. With prefix argument, prompt for ACTION-KIND." with grammar = '((:** "\\*\\*/?" eglot--glob-emit-**) (:* "\\*" eglot--glob-emit-*) (:? "\\?" eglot--glob-emit-?) - (:{} "{[^][/*{}]+}" eglot--glob-emit-{}) + (:{} "{[^][*{}]+}" eglot--glob-emit-{}) (:range "\\[\\^?[^][/,*{}]+\\]" eglot--glob-emit-range) (:literal "[^][,*?{}]+" eglot--glob-emit-self)) until (eobp) From e6fac3807870cc46ed2c2b97447265d1cb4c0cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 3 Feb 2021 10:41:40 +0000 Subject: [PATCH 562/771] Simplify dir-watching strategy of w/didchangewatchedfiles Instead of massaging the globPattern to match directories instead of files, which is fragile, gather the list of directoris to watch by matching the globPattern against every file recursively (except hidden files and dirs). This is still not 100% correct, but should do the right thing is most cases. Notably, if the correct dirs are being watched, the glob pattern is matched against all existing and new files in those directories, which does include hidden files. * eglot.el (eglot-register-capability): match file globs against files only. (eglot--files-recursively): Rename from eglot--directories-recursively. GitHub-reference: per https://github.com/joaotavora/eglot/issues/602 --- lisp/progmodes/eglot.el | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 90d973c6b63..80780f57f9a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2612,25 +2612,20 @@ at point. With prefix argument, prompt for ACTION-KIND." (let* (success (globs (mapcar (eglot--lambda ((FileSystemWatcher) globPattern) - (cons - (eglot--glob-compile globPattern t t) - (eglot--glob-compile - (replace-regexp-in-string "/[^/]*$" "/" globPattern) t t))) + (eglot--glob-compile globPattern t t)) watchers)) (dirs-to-watch - (cl-loop for dir in (eglot--directories-recursively) - when (cl-loop for g in globs - thereis (ignore-errors (funcall (cdr g) dir))) - collect dir))) + (cl-loop for f in (eglot--files-recursively) + when (cl-loop for g in globs thereis (funcall g f)) + collect (file-name-directory f) into dirs + finally (cl-return (delete-dups dirs))))) (cl-labels ((handle-event (event) (pcase-let ((`(,desc ,action ,file ,file1) event)) (cond ((and (memq action '(created changed deleted)) - (cl-find file (mapcar #'car globs) - :test (lambda (f glob) - (funcall glob f)))) + (cl-find file globs :test (lambda (f g) (funcall g f)))) (jsonrpc-notify server :workspace/didChangeWatchedFiles `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) @@ -2724,14 +2719,14 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) -(defun eglot--directories-recursively (&optional dir) +(defun eglot--files-recursively (&optional dir) "Because `directory-files-recursively' isn't complete in 26.3." (cons (setq dir (expand-file-name (or dir default-directory))) - (cl-loop - with default-directory = dir - with completion-regexp-list = '("^[^.]") - for f in (file-name-all-completions "" dir) - when (file-directory-p f) append (eglot--directories-recursively f)))) + (cl-loop with default-directory = dir + with completion-regexp-list = '("^[^.]") + for f in (file-name-all-completions "" dir) + if (file-name-directory f) append (eglot--files-recursively f) + else collect (expand-file-name f)))) ;;; Rust-specific From 514f80333b5d1aaff5c1ed1f03b3a49321381073 Mon Sep 17 00:00:00 2001 From: Jonathan del Strother Date: Sun, 21 Feb 2021 10:07:57 +0000 Subject: [PATCH 563/771] Silence messages while formatting markup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also fix https://github.com/joaotavora/eglot/issues/501. Prior to this, activating gfm-view-mode could echo messages like "markdown-mode math support enabled" to the minibuffer. Message are both silenced from from the minibuffer and the *Messaages* log. Co-authored-by: João Távora Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/502 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 80780f57f9a..13fe74a9057 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1196,7 +1196,9 @@ Doubles as an indicator of snippet support." (with-temp-buffer (setq-local markdown-fontify-code-blocks-natively t) (insert string) - (ignore-errors (delay-mode-hooks (funcall mode))) + (let ((inhibit-message t) + (message-log-max nil)) + (ignore-errors (delay-mode-hooks (funcall mode)))) (font-lock-ensure) (string-trim (filter-buffer-substring (point-min) (point-max)))))) From 93cbf54609909591af1bb864e12beda4141a4d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 24 Feb 2021 11:27:59 +0000 Subject: [PATCH 564/771] Handle null reply for textdocument/definition * eglot.el (eglot--lsp-xrefs-for-method): Handle null response from textDocument/definition & friends. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/625 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13fe74a9057..851f2e68ef9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2008,7 +2008,7 @@ Try to visit the target file for a richer summary line." (eglot--lambda ((Location) uri range) (collect (eglot--xref-make-match (symbol-name (symbol-at-point)) uri range))) - (if (vectorp response) response (list response)))))) + (if (vectorp response) response (and response (list response))))))) (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) "Helper for `eglot-find-declaration' & friends." From b3f31e0b657706baed13eeee2b6ae3eb1bd04049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 26 Feb 2021 18:49:59 +0000 Subject: [PATCH 565/771] Handle empty actions array in window/showmessagerequest * eglot.el (eglot-handle-request window/showMessageRequest): Handle empty actions. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/627 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 851f2e68ef9..98a1059e157 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1572,7 +1572,8 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-request (_server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" - (let ((label (completing-read + (let ((actions (append actions nil)) ;; gh#627 + (label (completing-read (concat (format (propertize "[eglot] Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) From 7c66a3e78957eea2fd5c80ffceeee4ad0c899927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 26 Feb 2021 19:30:30 +0000 Subject: [PATCH 566/771] Fixup last commit to fix * eglot.el (eglot-handle-request): Fixup. Use let* GitHub-reference: https://github.com/joaotavora/eglot/issues/627 --- lisp/progmodes/eglot.el | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 98a1059e157..86e0d01118f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1572,16 +1572,16 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-request (_server (_method (eql window/showMessageRequest)) &key type message actions) "Handle server request window/showMessageRequest" - (let ((actions (append actions nil)) ;; gh#627 - (label (completing-read - (concat - (format (propertize "[eglot] Server reports (type=%s): %s" - 'face (if (<= type 1) 'error)) - type message) - "\nChoose an option: ") - (or (mapcar (lambda (obj) (plist-get obj :title)) actions) - '("OK")) - nil t (plist-get (elt actions 0) :title)))) + (let* ((actions (append actions nil)) ;; gh#627 + (label (completing-read + (concat + (format (propertize "[eglot] Server reports (type=%s): %s" + 'face (if (<= type 1) 'error)) + type message) + "\nChoose an option: ") + (or (mapcar (lambda (obj) (plist-get obj :title)) actions) + '("OK")) + nil t (plist-get (elt actions 0) :title)))) (if label `(:title ,label) :null))) (cl-defmethod eglot-handle-notification From 5a4ca5fdf3082426a5d6aa8595492b3b1d50fd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 26 Feb 2021 20:11:08 +0000 Subject: [PATCH 567/771] Urify better See also https://microsoft.github.io/language-server-protocol/specifications/specification-current/#uri. * eglot.el (eglot--path-to-uri): use directory-file-name. GitHub-reference: per https://github.com/joaotavora/eglot/issues/627 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 86e0d01118f..017b8200bfa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1168,7 +1168,8 @@ If optional MARKER, return a marker instead" (defun eglot--path-to-uri (path) "URIfy PATH." (url-hexify-string - (concat "file://" (if (eq system-type 'windows-nt) "/") (file-truename path)) + (concat "file://" (if (eq system-type 'windows-nt) "/") + (directory-file-name (file-truename path))) url-path-allowed-chars)) (defun eglot--uri-to-path (uri) From fa3ab318fa0970b9590db92bf8230336fcbc61a7 Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Sat, 27 Feb 2021 11:19:35 +0100 Subject: [PATCH 568/771] Protect against empty uris on windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per https://github.com/joaotavora/eglot/issues/630. * eglot.el (eglot--uri-to-path): Check string length Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/610 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 017b8200bfa..0cb5839f6d5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1176,7 +1176,8 @@ If optional MARKER, return a marker instead" "Convert URI to a file path." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) (let ((retval (url-filename (url-generic-parse-url (url-unhex-string uri))))) - (if (eq system-type 'windows-nt) (substring retval 1) retval))) + (if (and (eq system-type 'windows-nt) (cl-plusp (length retval))) + (substring retval 1) retval))) (defun eglot--snippet-expansion-fn () "Compute a function to expand snippets. From a6229c50e8e00b895eed733f2626e6209043f504 Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Wed, 3 Mar 2021 11:08:24 +0100 Subject: [PATCH 569/771] Correctly protect against zero-length completion items Close https://github.com/joaotavora/eglot/issues/636. * eglot.el (eglot-completion-at-point): check for zero length string in proxy rather than the item. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/635 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0cb5839f6d5..610e57b5ad0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2133,7 +2133,7 @@ is not active." insertText) (t (string-trim-left label))))) - (unless (zerop (length item)) + (unless (zerop (length proxy)) (put-text-property 0 1 'eglot--lsp-item item proxy)) proxy)) items))))) From 2076d345655206254f6749cf710c150dfec313dd Mon Sep 17 00:00:00 2001 From: Brian Cully Date: Tue, 2 Mar 2021 16:13:07 -0500 Subject: [PATCH 570/771] Add tramp support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also close https://github.com/joaotavora/eglot/issues/463, close https://github.com/joaotavora/eglot/issues/84. Thanks to Brian Cully for the original simple idea. The basic technique is to pass :file-handler t to make-process, then tweak eglot--uri-to-path and eglot--path-to-uri, along with some other functions, to be aware of "trampy" paths". Crucially, a "stty hack" was needed. It has been encapsulated in a new a new eglot--cmd helper, which contains a comment explaining the hack. Co-authored-by: João Távora * eglot.el (eglot--executable-find): Shim two-arg executable-find function only available on Emacs 27. (eglot--guess-contact): Use eglot--executable-find. (eglot--cmd): New helper. (eglot--connect): Use eglot--cmd. Use :file-handler arg to make-process. (eglot--connect, eglot--path-to-uri): Be aware of trampy file names. * eglot-tests.el (eglot-tests--auto-detect-running-server-1): New helper. (eglot--guessing-contact): Better mock for executable-find. (eglot--tramp-test): New test. * NEWS.md: mention TRAMP support. * README.md: mention TRAMP support. GitHub-reference: close https://github.com/joaotavora/eglot/issues/637 --- lisp/progmodes/eglot.el | 57 ++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 610e57b5ad0..e0896c85017 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -244,6 +244,10 @@ let the buffer grow forever." (defconst eglot--{} (make-hash-table) "The empty JSON object.") +(defun eglot--executable-find (command &optional remote) + "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26." + (if (>= emacs-major-version 27) (executable-find command remote) + (executable-find command))) ;;; Message verification helpers @@ -753,7 +757,7 @@ be guessed." ((null guess) (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" managed-mode base-prompt)) - ((and program (not (executable-find program))) + ((and program (not (eglot--executable-find program t))) (concat (format "[eglot] I guess you want to run `%s'" program-guess) (format ", but I can't find `%s' in PATH!" program) @@ -878,6 +882,21 @@ received the initializing configuration. Each function is passed the server as an argument") +(defun eglot--cmd (contact) + "Helper for `eglot--connect'." + (if (file-remote-p default-directory) + ;; TODO: this seems like a bug, although it’s everywhere. For + ;; some reason, for remote connections only, over a pipe, we + ;; need to turn off line buffering on the tty. + ;; + ;; Not only does this seem like there should be a better way, + ;; but it almost certainly doesn’t work on non-unix systems. + (list "sh" "-c" + (string-join (cons "stty raw > /dev/null;" + (mapcar #'shell-quote-argument contact)) + " ")) + contact)) + (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." @@ -908,12 +927,13 @@ This docstring appeases checkdoc, that's all." (let ((default-directory default-directory)) (make-process :name readable-name - :command contact + :command (eglot--cmd contact) :connection-type 'pipe :coding 'utf-8-emacs-unix :noquery t :stderr (get-buffer-create - (format "*%s stderr*" readable-name))))))))) + (format "*%s stderr*" readable-name)) + :file-handler t))))))) (spread (lambda (fn) (lambda (server method params) (apply fn server method (append params nil))))) (server @@ -943,10 +963,15 @@ This docstring appeases checkdoc, that's all." (jsonrpc-async-request server :initialize - (list :processId (unless (eq (jsonrpc-process-type server) - 'network) - (emacs-pid)) - :rootPath (expand-file-name default-directory) + (list :processId + (unless (or (file-remote-p default-directory) + (eq (jsonrpc-process-type server) + 'network)) + (emacs-pid)) + ;; Maybe turn trampy `/ssh:foo@bar:/path/to/baz.py' + ;; into `/path/to/baz.py', so LSP groks it. + :rootPath (expand-file-name + (file-local-name default-directory)) :rootUri (eglot--path-to-uri default-directory) :initializationOptions (eglot-initialization-options server) @@ -1169,15 +1194,23 @@ If optional MARKER, return a marker instead" "URIfy PATH." (url-hexify-string (concat "file://" (if (eq system-type 'windows-nt) "/") - (directory-file-name (file-truename path))) + ;; Again watch out for trampy paths. + (directory-file-name (file-local-name (file-truename path)))) url-path-allowed-chars)) (defun eglot--uri-to-path (uri) - "Convert URI to a file path." + "Convert URI to file path, helped by `eglot--current-server'." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) - (let ((retval (url-filename (url-generic-parse-url (url-unhex-string uri))))) - (if (and (eq system-type 'windows-nt) (cl-plusp (length retval))) - (substring retval 1) retval))) + (let* ((retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) + (normalized (if (and (eq system-type 'windows-nt) + (cl-plusp (length retval))) + (substring retval 1) + retval)) + (server (eglot-current-server)) + (remote-prefix (and server + (file-remote-p + (project-root (eglot--project server)))))) + (concat remote-prefix normalized))) (defun eglot--snippet-expansion-fn () "Compute a function to expand snippets. From ff91ba70cd24e7469c7c171234363c82b1566cd6 Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Sat, 6 Mar 2021 21:18:48 +0100 Subject: [PATCH 571/771] Convert colon to hex in uri MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On windows, in the path portion of the URI, ':' must be hexified to '%3A'. In the URL scheme, the ':' stays. * eglot.el (eglot--uri-path-allowed-chars): define what characters are allowed in path portion of URI. * eglot.el (eglot--path-to-uri): ensure colon in 'file://' stays, but and others are hexified. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/638 --- lisp/progmodes/eglot.el | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e0896c85017..d4300e186be 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1190,13 +1190,19 @@ If optional MARKER, return a marker instead" (funcall eglot-move-to-column-function col))) (if marker (copy-marker (point-marker)) (point))))) +(defconst eglot--uri-path-allowed-chars + (let ((vec (copy-sequence url-path-allowed-chars))) + (aset vec ?: nil) ;; see github#639 + vec) + "Like `url-path-allows-chars' but more restrictive.") + (defun eglot--path-to-uri (path) "URIfy PATH." - (url-hexify-string - (concat "file://" (if (eq system-type 'windows-nt) "/") + (concat "file://" (if (eq system-type 'windows-nt) "/") + (url-hexify-string ;; Again watch out for trampy paths. - (directory-file-name (file-local-name (file-truename path)))) - url-path-allowed-chars)) + (directory-file-name (file-local-name (file-truename path))) + eglot--uri-path-allowed-chars))) (defun eglot--uri-to-path (uri) "Convert URI to file path, helped by `eglot--current-server'." From 8c0b2ca7cf76ec5215029ff0ab15f4bec1b1f12e Mon Sep 17 00:00:00 2001 From: Augusto Stoffel Date: Thu, 25 Feb 2021 15:48:41 +0100 Subject: [PATCH 572/771] Remove highlight overlays immediately when symbol edited MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot--highlight-piggyback): Add modification-hooks property to the created overlays. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/626 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d4300e186be..964658a8b23 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2421,7 +2421,8 @@ is not active." (eglot--range-region range))) (let ((ov (make-overlay beg end))) (overlay-put ov 'face 'eglot-highlight-symbol-face) - (overlay-put ov 'evaporate t) + (overlay-put ov 'modification-hooks + `(,(lambda (o &rest _) (delete-overlay o)))) ov))) highlights)))) :deferred :textDocument/documentHighlight) From 88b8b9364331573646cfc5b481d47ecd3323f0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 6 Mar 2021 21:15:33 +0000 Subject: [PATCH 573/771] Simplify eglot--apply-workspace-edit Suggested by Brian Leung. * eglot.el (eglot--apply-workspace-edit): simplify GitHub-reference: fix https://github.com/joaotavora/eglot/issues/620 --- lisp/progmodes/eglot.el | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 964658a8b23..f7632cb5996 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2530,8 +2530,7 @@ is not active." (eglot--dbind ((VersionedTextDocumentIdentifier) uri version) textDocument (list (eglot--uri-to-path uri) edits version))) - documentChanges)) - edit) + documentChanges))) (cl-loop for (uri edits) on changes by #'cddr do (push (list (eglot--uri-to-path uri) edits) prepared)) (if (or confirm @@ -2541,17 +2540,11 @@ is not active." (format "[eglot] Server wants to edit:\n %s\n Proceed? " (mapconcat #'identity (mapcar #'car prepared) "\n "))) (eglot--error "User cancelled server edit"))) - (while (setq edit (car prepared)) - (pcase-let ((`(,path ,edits ,version) edit)) - (with-current-buffer (find-file-noselect path) - (eglot--apply-text-edits edits version)) - (pop prepared)) - t) - (unwind-protect - (if prepared (eglot--warn "Caution: edits of files %s failed." - (mapcar #'car prepared)) - (eldoc) - (eglot--message "Edit successful!")))))) + (cl-loop for edit in prepared + for (path edits version) = edit + do (with-current-buffer (find-file-noselect path) + (eglot--apply-text-edits edits version)) + finally (eldoc) (eglot--message "Edit successful!"))))) (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." From 21b8ebf585498c0376e264f869ebeedb6ae4683a Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Sat, 6 Mar 2021 13:17:07 -0800 Subject: [PATCH 574/771] Indicate support for activeparameter * eglot.el (eglot-client-capabilities): Indicate :activeParameterSupport. Fixup of commit bdf57d5d4e888a6a7b4066b87497da8a8d9e36de. GitHub-reference: per https://github.com/joaotavora/eglot/issues/605 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f7632cb5996..14c0fbc5444 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -569,7 +569,8 @@ treated as in `eglot-dbind'." :signatureHelp (list :dynamicRegistration :json-false :signatureInformation `(:parameterInformation - (:labelOffsetSupport t))) + (:labelOffsetSupport t) + :activeParameterSupport t)) :references `(:dynamicRegistration :json-false) :definition `(:dynamicRegistration :json-false) :declaration `(:dynamicRegistration :json-false) From e43c1ee0d46f3af209a0a263fa059b1ed839b0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 6 Mar 2021 21:20:42 +0000 Subject: [PATCH 575/771] Autoload eglot-workspace-configuration's safe-l-v spec This is useful for those who edit files in a certain source tree where this directory-local variable is set, but without having yet loaded eglot.el. Those users would be bothered by the usual risky-local-variable prompt. * eglot.el (eglot-workspace-configuration): Add autoload cookie. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/555 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 14c0fbc5444..79b90886f6c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1848,6 +1848,7 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." SECTION should be a keyword or a string, value can be anything that can be converted to JSON.") +;;;###autoload (put 'eglot-workspace-configuration 'safe-local-variable 'listp) (defun eglot-signal-didChangeConfiguration (server) From a3e6b3b86c41c077b0939bc957d362f68f49f748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?bc=C2=B2?= Date: Mon, 15 Mar 2021 06:49:07 -0300 Subject: [PATCH 576/771] Add new command eglot-shutdown-all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also closes https://github.com/joaotavora/eglot/issues/644 Co-authored-by: João Távora Copyright-paperwork-exempt: yes * NEWS.md: mention new command * README.md (Commands and keybindings): mention new command. Tweak documentation for eglot-shutdown and eglot-reconnect. * eglot.el (eglot-shutdown): Tweak docstring. (eglot-shutdown-all): New command. GitHub-reference: close https://github.com/joaotavora/eglot/issues/643 --- lisp/progmodes/eglot.el | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 79b90886f6c..0341ffdecc6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -652,11 +652,12 @@ Interactively, read SERVER from the minibuffer unless there is only one and it's managing the current buffer. Forcefully quit it if it doesn't respond within TIMEOUT seconds. -Don't leave this function with the server still running. +TIMEOUT defaults to 1.5 seconds. Don't leave this function with +the server still running. If PRESERVE-BUFFERS is non-nil (interactively, when called with a prefix argument), do not kill events and output buffers of -SERVER. ." +SERVER." (interactive (list (eglot--read-server "Shutdown which server" (eglot-current-server)) t nil current-prefix-arg)) @@ -670,6 +671,13 @@ SERVER. ." (jsonrpc-shutdown server (not preserve-buffers)) (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) +(defun eglot-shutdown-all (&optional preserve-buffers) + "Politely ask all language servers to quit, in order. +PRESERVE-BUFFERS as in `eglot-shutdown', which see." + (interactive (list current-prefix-arg)) + (cl-loop for ss being the hash-values of eglot--servers-by-project + do (cl-loop for s in ss do (eglot-shutdown s nil preserve-buffers)))) + (defun eglot--on-shutdown (server) "Called by jsonrpc.el when SERVER is already dead." ;; Turn off `eglot--managed-mode' where appropriate. From 8a5f63d8bd91a2ff55c0e78ff8e893c79d0f5f94 Mon Sep 17 00:00:00 2001 From: "Johnathan C. Maudlin" <13183098+jcmdln@users.noreply.github.com> Date: Fri, 19 Mar 2021 16:10:43 -0400 Subject: [PATCH 577/771] Add support for zls, the zig language server * eglot.el (eglot-server-programs): Add zig-mode entry. * README.md (Connecting to a server): Mention zls. Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/646 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0341ffdecc6..5914f2ddd47 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -125,7 +125,8 @@ language-server/bin/php-language-server.php")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) - (f90-mode . ("fortls"))) + (f90-mode . ("fortls")) + (zig-mode . ("zls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE is a mode symbol, or a list of mode symbols. The associated From 602004dbb5286e20fffae9c034b89aab994ad826 Mon Sep 17 00:00:00 2001 From: rvs314 <71688932+rvs314@users.noreply.github.com> Date: Fri, 26 Mar 2021 06:08:03 -0400 Subject: [PATCH 578/771] Offer better control over "languageid" value sent to lsp Handles the issue of languages whose major mode has a different name than the name that the LSP server expects for the language. One can now: (put 'favourite-major-mode 'eglot-language-id "foobarbaz") And "foobarbaz" will be used as the LSP "languageId" value. * eglot.el (eglot--TextDocumentItem): Consult 'eglot-language-id. Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/525 --- lisp/progmodes/eglot.el | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5914f2ddd47..03e8baa8b76 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1741,9 +1741,11 @@ THINGS are either registrations or unregisterations (sic)." (append (eglot--VersionedTextDocumentIdentifier) (list :languageId - (if (string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode)) - "unknown") + (cond + ((get major-mode 'eglot-language-id)) + ((string-match "\\(.*\\)-mode" (symbol-name major-mode)) + (match-string 1 (symbol-name major-mode))) + (t "unknown")) :text (eglot--widening (buffer-substring-no-properties (point-min) (point-max)))))) From dcbb5a8d0bf3936363d1cfcb5b074199f3a67353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 2 Apr 2021 00:21:27 +0100 Subject: [PATCH 579/771] Generalize eglot-flymake-backend Loosen coupling between eglot-flymake-backend and flymake-mode. The flymake-mode check in 'eglot-handle-notification publishDiagnostics' was a hack (and it wasn't even functioning correctly on M-x eglot-shutdown/eglot-reconnect). This should also allow eglot-flymake-backend to be driven by diagnostic-annotating frontends other than Flymake, such as the popular Flycheck package. * eglot.el (eglot--managed-mode): Use eglot--report-to-flymake. (eglot-handle-notification textDocument/publishDiagnostics): Use eglot--report-to-flymake. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/596 --- lisp/progmodes/eglot.el | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 03e8baa8b76..c2363886769 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1445,7 +1445,9 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (cl-loop for (var . saved-binding) in eglot--saved-bindings do (set (make-local-variable var) saved-binding)) (remove-function (local 'imenu-create-index-function) #'eglot-imenu) - (setq eglot--current-flymake-report-fn nil) + (when eglot--current-flymake-report-fn + (eglot--report-to-flymake nil) + (setq eglot--current-flymake-report-fn nil)) (let ((server eglot--cached-server)) (setq eglot--cached-server nil) (when server @@ -1680,17 +1682,8 @@ COMMAND is a symbol naming the command." (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) into diags - finally (cond ((and flymake-mode eglot--current-flymake-report-fn) - (save-restriction - (widen) - (funcall eglot--current-flymake-report-fn diags - ;; If the buffer hasn't changed since last - ;; call to the report function, flymake won't - ;; delete old diagnostics. Using :region - ;; keyword forces flymake to delete - ;; them (github#159). - :region (cons (point-min) (point-max)))) - (setq eglot--unreported-diagnostics nil)) + finally (cond (eglot--current-flymake-report-fn + (eglot--report-to-flymake diags)) (t (setq eglot--unreported-diagnostics (cons t diags)))))) (jsonrpc--debug server "Diagnostics received for unvisited %s" uri))) @@ -1970,13 +1963,28 @@ When called interactively, use the currently active server" :textDocument (eglot--TextDocumentIdentifier)))) (defun eglot-flymake-backend (report-fn &rest _more) - "An EGLOT Flymake backend. -Calls REPORT-FN maybe if server publishes diagnostics in time." + "A Flymake backend for Eglot. +Calls REPORT-FN (or arranges for it to be called) when the server +publishes diagnostics. Between calls to this function, REPORT-FN +may be called multiple times (respecting the protocol of +`flymake-backend-functions')." (setq eglot--current-flymake-report-fn report-fn) ;; Report anything unreported (when eglot--unreported-diagnostics - (funcall report-fn (cdr eglot--unreported-diagnostics)) - (setq eglot--unreported-diagnostics nil))) + (eglot--report-to-flymake (cdr eglot--unreported-diagnostics)))) + +(defun eglot--report-to-flymake (diags) + "Internal helper for `eglot-flymake-backend'." + (save-restriction + (widen) + (funcall eglot--current-flymake-report-fn diags + ;; If the buffer hasn't changed since last + ;; call to the report function, flymake won't + ;; delete old diagnostics. Using :region + ;; keyword forces flymake to delete + ;; them (github#159). + :region (cons (point-min) (point-max)))) + (setq eglot--unreported-diagnostics nil)) (defun eglot-xref-backend () "EGLOT xref backend." 'eglot) From 83b993258b9c96cde165b4055fe1d32820907e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 10 Apr 2021 14:31:14 +0100 Subject: [PATCH 580/771] Attempt to speed up initial directory/glob correspondence In https://github.com/joaotavora/eglot/issues/602, not only a new glob processing system was implemented, but also a new, more correct, way to look for directories that might hold files matched by one of these globs. Answering this question is important because the file watchers for 'workspace/didChangeWatchedFiles' are placed on a per-directory basis. Previously, a glob such as /foo/**/bar/*.el would fail to produce practical file-watching effects because /foo/**/bar/ isn't really a directory. However, answering this question is also expensive, as the globs sent by the LSP server are meant to match files, not directories. The only way is to list all files under the project's root directory and test each glob on each one. If it matches at least one file, that file's directory is meant to be watched. We suspect that in https://github.com/joaotavora/eglot/issues/645 and https://github.com/joaotavora/eglot/issues/633 we are falling victim to LSP server who serve a tremendous unoptimized number of globs, one for each file. So instead of sending just '/foo/**/bar/*.el' they send '/foo/**/bar/quux.el', '/foo/**/bar/quuz.el', etc... which would tremendeously slow down the process. But this is only a suspicion. This commit tries some simple optimizations: if a directory is known to be watch-worthy becasue one of its files matched a single glob, no more files under that directory are tried. This should help somewhat. Also fixed a bug in 'eglot--files-recursively', though I suspect that doesn't make that much of a difference. * eglot.el (eglot--directories-matched-by-globs): New helper. (eglot--files-recursively): Fix bug. GitHub-reference: per https://github.com/joaotavora/eglot/issues/645 --- lisp/progmodes/eglot.el | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c2363886769..59804da9e4d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2674,10 +2674,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (eglot--glob-compile globPattern t t)) watchers)) (dirs-to-watch - (cl-loop for f in (eglot--files-recursively) - when (cl-loop for g in globs thereis (funcall g f)) - collect (file-name-directory f) into dirs - finally (cl-return (delete-dups dirs))))) + (eglot--directories-matched-by-globs default-directory globs))) (cl-labels ((handle-event (event) @@ -2784,9 +2781,23 @@ If NOERROR, return predicate, else erroring function." (cl-loop with default-directory = dir with completion-regexp-list = '("^[^.]") for f in (file-name-all-completions "" dir) - if (file-name-directory f) append (eglot--files-recursively f) + if (file-directory-p f) append (eglot--files-recursively f) else collect (expand-file-name f)))) +(defun eglot--directories-matched-by-globs (dir globs) + "Discover subdirectories of DIR with files matched by one of GLOBS. +Each element of GLOBS is either an uncompiled glob-string or a +compiled glob." + (setq globs (cl-loop for g in globs + collect (if (stringp g) (eglot--glob-compile g t t) g))) + (cl-loop for f in (eglot--files-recursively dir) + for fdir = (file-name-directory f) + when (and + (not (member fdir dirs)) + (cl-loop for g in globs thereis (funcall g f))) + collect fdir into dirs + finally (cl-return (delete-dups dirs)))) + ;;; Rust-specific ;;; From 355f1b5f49cc091ce48a50e534853225f375936a Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Mon, 29 Mar 2021 22:17:07 +0100 Subject: [PATCH 581/771] Highlight relevant part of xref hits using xref-match face Also close https://github.com/joaotavora/eglot/issues/657. (eglot--xref-make-match): Use face 'xref-match instead of 'highlight. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/650 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 59804da9e4d..57c065273b7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2020,7 +2020,7 @@ Try to visit the target file for a richer summary line." (substring (buffer-substring bol (point-at-eol))) (hi-beg (- beg bol)) (hi-end (- (min (point-at-eol) end) bol))) - (add-face-text-property hi-beg hi-end 'highlight + (add-face-text-property hi-beg hi-end 'xref-match t substring) (list substring (1+ (current-line)) (eglot-current-column) (- end beg)))))) From 7ae862de9e85bf58aba1bda0c119459a6fbf273d Mon Sep 17 00:00:00 2001 From: Mohsin Kaleem Date: Mon, 29 Mar 2021 22:32:33 +0100 Subject: [PATCH 582/771] Add :company-kind to eglot-completion-at-point * eglot.el (eglot-completion-at-point): Add a :company-kind field to the completion-at-point function so that company can associate completion candidates with lsp types. GitHub-reference: close https://github.com/joaotavora/eglot/issues/652 --- lisp/progmodes/eglot.el | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5914f2ddd47..579ed27913e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2236,6 +2236,13 @@ is not active." (concat " " (propertize annotation 'face 'font-lock-function-name-face)))))) + :company-kind + ;; Associate each lsp-item with a lsp-kind symbol. + (lambda (proxy) + (when-let* ((lsp-item (get-text-property 0 'eglot--lsp-item proxy)) + (kind (alist-get (plist-get lsp-item :kind) + eglot--kind-names))) + (intern (downcase kind)))) :company-doc-buffer (lambda (proxy) (let* ((documentation From 0d89dd73ff02bd0f70e2fcca0c2dcedf821faeb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 13 Apr 2021 01:16:31 +0100 Subject: [PATCH 583/771] Add a passing test demonstrating clangd + tramp works ... It works at least within the minimal, well-controlled reproducible settings of this test. Maybe if we knew something more about the setup of the user who submitted this report we would be able to concoct a failing test, but we don't. * eglot-tests.el (subr-x): Require it (eglot--make-file-or-dir): Return expanded file name. (eglot-tests--lsp-abiding-column-1): New helper. (eglot-lsp-abiding-column): Use it. (eglot--tramp-test): Fix `skip-unless` condition. (eglot--tramp-test-2): New test. GitHub-reference: per https://github.com/joaotavora/eglot/issues/667 --- lisp/progmodes/eglot.el | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cbde1b7a4c7..20f59956f81 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -25,7 +25,7 @@ ;;; Commentary: ;; Simply M-x eglot should be enough to get you started, but here's a -;; little info (see the accompanying README.md or the URL for more). + ;; little info (see the accompanying README.md or the URL for more). ;; ;; M-x eglot starts a server via a shell-command guessed from ;; `eglot-server-programs', using the current major-mode (for whatever @@ -2791,6 +2791,15 @@ If NOERROR, return predicate, else erroring function." if (file-directory-p f) append (eglot--files-recursively f) else collect (expand-file-name f)))) +(defun eglot--directories-recursively (&optional dir) + "Because `directory-files-recursively' isn't complete in 26.3." + (cons (setq dir (expand-file-name (or dir default-directory))) + (cl-loop with default-directory = dir + with completion-regexp-list = '("^[^.]") + for f in (file-name-all-completions "" dir) + if (file-directory-p f) append (eglot--files-recursively f) + else collect (expand-file-name f)))) + (defun eglot--directories-matched-by-globs (dir globs) "Discover subdirectories of DIR with files matched by one of GLOBS. Each element of GLOBS is either an uncompiled glob-string or a From 5b33fe06c506d6a83f6ef77851478033de840b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 20 Apr 2021 18:39:54 +0100 Subject: [PATCH 584/771] Make eglot-current-server work in notification handlers * eglot.el (eglot--connect): Ensure `eglot--cached-server` bound when calling notification/request methods. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/670 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 20f59956f81..f926709e8cf 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -945,7 +945,8 @@ This docstring appeases checkdoc, that's all." (format "*%s stderr*" readable-name)) :file-handler t))))))) (spread (lambda (fn) (lambda (server method params) - (apply fn server method (append params nil))))) + (let ((eglot--cached-server server)) + (apply fn server method (append params nil)))))) (server (apply #'make-instance class From f634580f1bb8c2e329810adfef42f35503cb8848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 23 Apr 2021 08:49:02 +0100 Subject: [PATCH 585/771] Declare eglot--cached-server before use Per https://github.com/joaotavora/eglot/issues/670. Otherwise the dynamic binding of it in in eglot--connect won't work. * eglot.el (eglot--cached-server): Move up. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/673 --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f926709e8cf..3373be2256c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -907,6 +907,9 @@ Each function is passed the server as an argument") " ")) contact)) +(defvar-local eglot--cached-server nil + "A cached reference to the current EGLOT server.") + (defun eglot--connect (managed-major-mode project class contact) "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." @@ -1378,9 +1381,6 @@ For example, to keep your Company customization use (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) (setq-local ,symbol ,binding))) -(defvar-local eglot--cached-server nil - "A cached reference to the current EGLOT server.") - (defun eglot-managed-p () "Tell if current buffer is managed by EGLOT." eglot--managed-mode) From f3e2ca5bd5f4ee3f5a32e85dc9cb27eaa982b41b Mon Sep 17 00:00:00 2001 From: Steve Purcell Date: Mon, 26 Apr 2021 10:51:44 +1200 Subject: [PATCH 586/771] Switch default langserver for ocaml to ocamllsp The repo for ocaml-language-server has been archived and inactive for quite some time: https://github.com/ocaml-lsp/ocaml-language-server Meanwhile, ocaml-lsp is the generally-preferred option, and is actively maintained in the ocaml org itself: https://github.com/ocaml/ocaml-lsp/ * eglot.el (eglot-server-programs): switch caml-mode entry. GitHub-reference: close https://github.com/joaotavora/eglot/issues/677 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3373be2256c..3348054264b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -105,7 +105,7 @@ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) ((caml-mode tuareg-mode reason-mode) - . ("ocaml-language-server" "--stdio")) + . ("ocamllsp")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) (haskell-mode From a1fb92543ccd21375f21143939a29445016d56ee Mon Sep 17 00:00:00 2001 From: Augusto Stoffel Date: Wed, 28 Apr 2021 11:41:19 +0100 Subject: [PATCH 587/771] Add a completion-category-defaults entry Setting completion-styles buffer-locally is harder to customize and can break some completion UIs. Emacs bughttps://github.com/joaotavora/eglot/issues/48073 * eglot.el: Add a completion-category-defaults entry, if applicable. (eglot--managed-mode): Don't set `completion-styles' (eglot-completion-at-point): Add style metadata to completion table. --- lisp/progmodes/eglot.el | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3348054264b..122a76b0359 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -222,6 +222,10 @@ let the buffer grow forever." :type '(choice (const :tag "Don't show confirmation prompt" nil) (symbol :tag "Show confirmation prompt" 'confirm))) +;; Customizable via `completion-category-overrides'. +(when (assoc 'flex completion-styles-alist) + (add-to-list 'completion-category-defaults '(eglot (styles flex basic)))) + ;;; Constants ;;; @@ -1421,8 +1425,6 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend)) (eglot--setq-saving company-backends '(company-capf)) (eglot--setq-saving company-tooltip-align-annotations t) - (when (assoc 'flex completion-styles-alist) - (eglot--setq-saving completion-styles '(flex basic))) (unless (eglot--stay-out-of-p 'imenu) (add-function :before-until (local 'imenu-create-index-function) #'eglot-imenu)) @@ -2166,7 +2168,8 @@ is not active." (get-text-property 0 'eglot--lsp-item c) :sortText) ""))))) - (metadata `(metadata . ((display-sort-function . ,sort-completions)))) + (metadata `(metadata (category . eglot) + (display-sort-function . ,sort-completions))) resp items (cached-proxies :none) (proxies (lambda () From edf75e87cac1d72745955cd8965af4ce586c1bb8 Mon Sep 17 00:00:00 2001 From: Steve Purcell Date: Fri, 30 Apr 2021 11:09:04 +1200 Subject: [PATCH 588/771] Allow lsp languageid to be overridden via eglot-server-programs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close https://github.com/joaotavora/eglot/issues/678. Per https://github.com/joaotavora/eglot/issues/677 * eglot-tests.el (eglot--guessing-contact): Add GUESSED-LANG-ID-SYM param. (eglot-server-programs-guess-lang): New test. * eglot.el (eglot-server-programs): Augment entries for caml-mode and tuareg-mode. Enhance docstring. (eglot--lookup-mode): New helper. (eglot--guess-contact): Call eglot--lookup-mode. (eglot, eglot-reconnect): Pass language-id to eglot--connect (eglot--connect): Receive LANGUAGE-ID (eglot--TextDocumentItem): Simplify. Use `eglot--current-server-or-lose' * README.md (Handling quirky servers): Mention new feature. Co-authored-by: João Távora --- lisp/progmodes/eglot.el | 80 ++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 122a76b0359..b1917631981 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -104,7 +104,8 @@ . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) ((c++-mode c-mode) . ("ccls")) - ((caml-mode tuareg-mode reason-mode) + (((caml-mode :language-id "ocaml") + (tuareg-mode :language-id "ocaml") reason-mode) . ("ocamllsp")) (ruby-mode . ("solargraph" "socket" "--port" :autoport)) @@ -129,9 +130,23 @@ language-server/bin/php-language-server.php")) (zig-mode . ("zls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE -is a mode symbol, or a list of mode symbols. The associated -CONTACT specifies how to connect to a server for managing buffers -of those modes. CONTACT can be: +identifies the buffers that are to be managed by a specific +language server. The associated CONTACT specifies how to connect +to a server for those buffers. + +MAJOR-MODE can be: + +* In the most common case, a symbol such as `c-mode'; + +* A list (MAJOR-MODE-SYMBOL :LANGUAGE-ID ID) where + MAJOR-MODE-SYMBOL is the aforementioned symbol and ID is a + string identifying the language to the server; + +* A list combining the previous two alternatives, meaning + multiple major modes will be associated with a single server + program. + +CONTACT can be: * In the most common case, a list of strings (PROGRAM [ARGS...]). PROGRAM is called with ARGS and is expected to serve LSP requests @@ -612,6 +627,9 @@ treated as in `eglot-dbind'." (major-mode :documentation "Major mode symbol." :accessor eglot--major-mode) + (language-id + :documentation "Language ID string for the mode." + :accessor eglot--language-id) (capabilities :documentation "JSON object containing server capabilities." :accessor eglot--capabilities) @@ -720,9 +738,29 @@ PRESERVE-BUFFERS as in `eglot-shutdown', which see." (defvar eglot--command-history nil "History of CONTACT arguments to `eglot'.") +(defun eglot--lookup-mode (mode) + "Lookup `eglot-server-programs' for MODE. +Return (LANGUAGE-ID . CONTACT-PROXY). If not specified, +LANGUAGE-ID is determined from MODE." + (cl-loop + for (modes . contact) in eglot-server-programs + thereis (cl-some + (lambda (spec) + (cl-destructuring-bind (probe &key language-id &allow-other-keys) + (if (consp spec) spec (list spec)) + (and (provided-mode-derived-p mode probe) + (cons + (or language-id + (or (get mode 'eglot-language-id) + (get spec 'eglot-language-id) + (string-remove-suffix "-mode" (symbol-name mode)))) + contact)))) + (if (or (symbolp modes) (keywordp (cadr modes))) + (list modes) modes)))) + (defun eglot--guess-contact (&optional interactive) "Helper for `eglot'. -Return (MANAGED-MODE PROJECT CLASS CONTACT). If INTERACTIVE is +Return (MANAGED-MODE PROJECT CLASS CONTACT LANG-ID). If INTERACTIVE is non-nil, maybe prompt user, else error as soon as something can't be guessed." (let* ((guessed-mode (if buffer-file-name major-mode)) @@ -740,11 +778,9 @@ be guessed." (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) (project (or (project-current) `(transient . ,default-directory))) - (guess (cdr (assoc managed-mode eglot-server-programs - (lambda (m1 m2) - (cl-find - m2 (if (listp m1) m1 (list m1)) - :test #'provided-mode-derived-p))))) + (lang-id-and-guess (eglot--lookup-mode guessed-mode)) + (language-id (car lang-id-and-guess)) + (guess (cdr lang-id-and-guess)) (guess (if (functionp guess) (funcall guess interactive) guess)) @@ -791,10 +827,11 @@ be guessed." :test #'equal)))) guess (eglot--error "Couldn't guess for `%s'!" managed-mode)))) - (list managed-mode project class contact))) + (list managed-mode project class contact language-id))) ;;;###autoload -(defun eglot (managed-major-mode project class contact &optional interactive) +(defun eglot (managed-major-mode project class contact language-id + &optional interactive) "Manage a project with a Language Server Protocol (LSP) server. The LSP server of CLASS is started (or contacted) via CONTACT. @@ -821,6 +858,9 @@ CONTACT specifies how to contact the server. It is a keyword-value plist used to initialize CLASS or a plain list as described in `eglot-server-programs', which see. +LANGUAGE-ID is the language ID string to send to the server for +MANAGED-MAJOR-MODE, which matters to a minority of servers. + INTERACTIVE is t if called interactively." (interactive (append (eglot--guess-contact t) '(t))) (let* ((current-server (eglot-current-server)) @@ -830,7 +870,7 @@ INTERACTIVE is t if called interactively." (y-or-n-p "[eglot] Live process found, reconnect instead? ")) (eglot-reconnect current-server interactive) (when live-p (ignore-errors (eglot-shutdown current-server))) - (eglot--connect managed-major-mode project class contact)))) + (eglot--connect managed-major-mode project class contact language-id)))) (defun eglot-reconnect (server &optional interactive) "Reconnect to SERVER. @@ -841,7 +881,8 @@ INTERACTIVE is t if called interactively." (eglot--connect (eglot--major-mode server) (eglot--project server) (eieio-object-class-name server) - (eglot--saved-initargs server)) + (eglot--saved-initargs server) + (eglot--language-id server)) (eglot--message "Reconnected!")) (defvar eglot--managed-mode) ; forward decl @@ -914,8 +955,8 @@ Each function is passed the server as an argument") (defvar-local eglot--cached-server nil "A cached reference to the current EGLOT server.") -(defun eglot--connect (managed-major-mode project class contact) - "Connect to MANAGED-MAJOR-MODE, PROJECT, CLASS and CONTACT. +(defun eglot--connect (managed-major-mode project class contact language-id) + "Connect to MANAGED-MAJOR-MODE, LANGUAGE-ID, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." (let* ((default-directory (project-root project)) (nickname (file-name-base (directory-file-name default-directory))) @@ -969,6 +1010,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) (setf (eglot--major-mode server) managed-major-mode) + (setf (eglot--language-id server) language-id) (setf (eglot--inferior-process server) autostart-inferior-process) (run-hook-with-args 'eglot-server-initialized-hook server) ;; Now start the handshake. To honour `eglot-sync-connect' @@ -1737,11 +1779,7 @@ THINGS are either registrations or unregisterations (sic)." (append (eglot--VersionedTextDocumentIdentifier) (list :languageId - (cond - ((get major-mode 'eglot-language-id)) - ((string-match "\\(.*\\)-mode" (symbol-name major-mode)) - (match-string 1 (symbol-name major-mode))) - (t "unknown")) + (eglot--language-id (eglot--current-server-or-lose)) :text (eglot--widening (buffer-substring-no-properties (point-min) (point-max)))))) From ded0aa0bfc2a4213ec7b938f356478d2abd21dc8 Mon Sep 17 00:00:00 2001 From: Steve Purcell Date: Fri, 30 Apr 2021 20:32:07 +1200 Subject: [PATCH 589/771] Fix emacs 28 warning by avoiding positional args in define-minor-mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot--managed-mode): Avoid positional args. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/685 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b1917631981..29024ad3fb6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1440,7 +1440,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (define-minor-mode eglot--managed-mode "Mode for source buffers managed by some EGLOT project." - nil nil eglot-mode-map + :init-value nil :lighter nil :keymap eglot-mode-map (cond (eglot--managed-mode (add-hook 'after-change-functions 'eglot--after-change nil t) From 68baa57143ed4f338778053d9aa3120ed5d59300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 13 May 2021 10:09:20 +0100 Subject: [PATCH 590/771] Provide context for finer project-find-functions * eglot.el (eglot--guess-contact): Use eglot--current-project. (eglot): Adjust docstring. (eglot-lsp-context): New variable. (eglot--current-project): New helper. (eglot--maybe-activate-editing-mode, eglot--eclipse-jdt-contact): Use eglot--current-project. GitHub-reference: per https://github.com/joaotavora/eglot/issues/687 --- lisp/progmodes/eglot.el | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 29024ad3fb6..fc82367f8e1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -777,7 +777,6 @@ be guessed." ((not guessed-mode) (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) - (project (or (project-current) `(transient . ,default-directory))) (lang-id-and-guess (eglot--lookup-mode guessed-mode)) (language-id (car lang-id-and-guess)) (guess (cdr lang-id-and-guess)) @@ -827,7 +826,21 @@ be guessed." :test #'equal)))) guess (eglot--error "Couldn't guess for `%s'!" managed-mode)))) - (list managed-mode project class contact language-id))) + (list managed-mode (eglot--current-project) class contact language-id))) + +(defvar eglot-lsp-context) +(put 'eglot-lsp-context 'variable-documentation + "Dynamically non-nil when searching for projects in LSP context.") + +(defun eglot--current-project () + "Return a project object for Eglot's LSP purposes. +This relies on `project-current' and thus on +`project-find-functions'. Functions in the latter +variable (which see) can query the value `eglot-lsp-context' to +decide whether a given directory is a project containing a +suitable root directory for a given LSP server's purposes." + (let ((eglot-lsp-context t)) + (or (project-current) `(transient . ,default-directory)))) ;;;###autoload (defun eglot (managed-major-mode project class contact language-id @@ -844,13 +857,16 @@ exchanged periodically to provide enhanced code-analysis via Interactively, the command attempts to guess MANAGED-MAJOR-MODE from current buffer, CLASS and CONTACT from -`eglot-server-programs' and PROJECT from `project-current'. If -it can't guess, the user is prompted. With a single +`eglot-server-programs' and PROJECT from +`project-find-functions'. The search for active projects in this +context binds `eglot-lsp-context' (which see). + +If it can't guess, the user is prompted. With a single \\[universal-argument] prefix arg, it always prompt for COMMAND. With two \\[universal-argument] prefix args, also prompts for MANAGED-MAJOR-MODE. -PROJECT is a project instance as returned by `project-current'. +PROJECT is a project object as returned by `project-current'. CLASS is a subclass of `eglot-lsp-server'. @@ -1537,8 +1553,7 @@ If it is activated, also signal textDocument/didOpen." eglot--cached-server (setq eglot--cached-server (cl-find major-mode - (gethash (or (project-current) - `(transient . ,default-directory)) + (gethash (eglot--current-project) eglot--servers-by-project) :key #'eglot--major-mode)))) (setq eglot--unreported-diagnostics `(:just-opened . nil)) @@ -2939,9 +2954,8 @@ If INTERACTIVE, prompt user for details." ((string= system-type "darwin") "config_mac") ((string= system-type "windows-nt") "config_win") (t "config_linux")))) - (project (or (project-current) `(transient . ,default-directory))) (workspace - (expand-file-name (md5 (project-root project)) + (expand-file-name (md5 (project-root (eglot--current-project))) (concat user-emacs-directory "eglot-eclipse-jdt-cache")))) (unless jar From fe9d6daa57102c7daebdcced2d7b6253cecb047c Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Thu, 13 May 2021 08:55:31 -0700 Subject: [PATCH 591/771] Correct path/uri when using tramp from ms windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Távora * eglot.el (eglot--connect): Ensure drive letter doesn't sneak into rootPath. (eglot--path-to-uri): Only add a leading "/" for local MS Windows paths. (eglot--uri-to-path): Only remove leading "/" from local MS Windows paths. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/679 --- lisp/progmodes/eglot.el | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fc82367f8e1..f17e795bfb5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1046,8 +1046,8 @@ This docstring appeases checkdoc, that's all." (emacs-pid)) ;; Maybe turn trampy `/ssh:foo@bar:/path/to/baz.py' ;; into `/path/to/baz.py', so LSP groks it. - :rootPath (expand-file-name - (file-local-name default-directory)) + :rootPath (file-local-name + (expand-file-name default-directory)) :rootUri (eglot--path-to-uri default-directory) :initializationOptions (eglot-initialization-options server) @@ -1274,24 +1274,31 @@ If optional MARKER, return a marker instead" (defun eglot--path-to-uri (path) "URIfy PATH." - (concat "file://" (if (eq system-type 'windows-nt) "/") - (url-hexify-string - ;; Again watch out for trampy paths. - (directory-file-name (file-local-name (file-truename path))) - eglot--uri-path-allowed-chars))) + (let ((truepath (file-truename path))) + (concat "file://" + ;; Add a leading "/" for local MS Windows-style paths. + (if (and (eq system-type 'windows-nt) + (not (file-remote-p truepath))) + "/") + (url-hexify-string + ;; Again watch out for trampy paths. + (directory-file-name (file-local-name truepath)) + eglot--uri-path-allowed-chars)))) (defun eglot--uri-to-path (uri) "Convert URI to file path, helped by `eglot--current-server'." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) - (let* ((retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) - (normalized (if (and (eq system-type 'windows-nt) - (cl-plusp (length retval))) - (substring retval 1) - retval)) - (server (eglot-current-server)) + (let* ((server (eglot-current-server)) (remote-prefix (and server (file-remote-p - (project-root (eglot--project server)))))) + (project-root (eglot--project server))))) + (retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) + ;; Remove the leading "/" for local MS Windows-style paths. + (normalized (if (and (not remote-prefix) + (eq system-type 'windows-nt) + (cl-plusp (length retval))) + (substring retval 1) + retval))) (concat remote-prefix normalized))) (defun eglot--snippet-expansion-fn () From 7eddb6f950b0a067a55347600d499cae608aa728 Mon Sep 17 00:00:00 2001 From: Michael Livshin Date: Sat, 15 Dec 2018 01:17:32 +0000 Subject: [PATCH 592/771] Manage cross-referenced files outside project in same server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close https://github.com/joaotavora/eglot/issues/686, Close https://github.com/joaotavora/eglot/issues/695. Co-authored-by: João Távora * eglot.el (eglot-extend-to-xref): new defcustom, default to nil. (eglot--servers-by-xrefed-file): new hash table, mapping file names to servers. (eglot--managed-mode): use eglot-current-server, instead of eglot--cached-server directly. (eglot--current-server-or-lose): ditto. (eglot--maybe-activate-editing-mode): ditto. (eglot-current-server): move all cached-server update logic here -- if eglot--cached-server is nil, try to find it using current project or (optionally) xref location. (eglot--xref-make-match): record the xref location. * README.md (Customization): Mention new defcustom. * NEWS.md: Mention new feature GitHub-reference: fix https://github.com/joaotavora/eglot/issues/76 --- lisp/progmodes/eglot.el | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f17e795bfb5..db468d83c14 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -237,6 +237,10 @@ let the buffer grow forever." :type '(choice (const :tag "Don't show confirmation prompt" nil) (symbol :tag "Show confirmation prompt" 'confirm))) +(defcustom eglot-extend-to-xref nil + "If non-nil, activate Eglot in cross-referenced non-project files." + :type 'boolean) + ;; Customizable via `completion-category-overrides'. (when (assoc 'flex completion-styles-alist) (add-to-list 'completion-category-defaults '(eglot (styles flex basic)))) @@ -832,6 +836,9 @@ be guessed." (put 'eglot-lsp-context 'variable-documentation "Dynamically non-nil when searching for projects in LSP context.") +(defvar eglot--servers-by-xrefed-file + (make-hash-table :test 'equal :weakness 'value)) + (defun eglot--current-project () "Return a project object for Eglot's LSP purposes. This relies on `project-current' and thus on @@ -1495,7 +1502,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") #'eglot-imenu)) (flymake-mode 1) (eldoc-mode 1) - (cl-pushnew (current-buffer) (eglot--managed-buffers eglot--cached-server))) + (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server)))) (t (remove-hook 'after-change-functions 'eglot--after-change t) (remove-hook 'before-change-functions 'eglot--before-change t) @@ -1533,11 +1540,19 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (defun eglot-current-server () "Return logical EGLOT server for current buffer, nil if none." - eglot--cached-server) + (setq eglot--cached-server + (or eglot--cached-server + (cl-find major-mode + (gethash (eglot--current-project) eglot--servers-by-project) + :key #'eglot--major-mode) + (and eglot-extend-to-xref + buffer-file-name + (gethash (expand-file-name buffer-file-name) + eglot--servers-by-xrefed-file))))) (defun eglot--current-server-or-lose () "Return current logical EGLOT server connection or error." - (or eglot--cached-server + (or (eglot-current-server) (jsonrpc-error "No current JSON-RPC connection"))) (defvar-local eglot--unreported-diagnostics nil @@ -1555,14 +1570,7 @@ If it is activated, also signal textDocument/didOpen." (unless eglot--managed-mode ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. - (when (and buffer-file-name - (or - eglot--cached-server - (setq eglot--cached-server - (cl-find major-mode - (gethash (eglot--current-project) - eglot--servers-by-project) - :key #'eglot--major-mode)))) + (when (and buffer-file-name (eglot-current-server)) (setq eglot--unreported-diagnostics `(:just-opened . nil)) (eglot--managed-mode) (eglot--signal-textDocument/didOpen)))) @@ -2101,6 +2109,8 @@ Try to visit the target file for a richer summary line." (start-pos (cl-getf start :character)) (end-pos (cl-getf (cl-getf range :end) :character))) (list name line start-pos (- end-pos start-pos))))))) + (setf (gethash (expand-file-name file) eglot--servers-by-xrefed-file) + (eglot--current-server-or-lose)) (xref-make-match summary (xref-make-file-location file line column) length))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) From 210b52859e71b81c6dc022ea3e07b1a68bc978d3 Mon Sep 17 00:00:00 2001 From: Augusto Stoffel Date: Sat, 22 May 2021 12:53:38 +0200 Subject: [PATCH 593/771] Allow staying out of flymake-mode, eldoc-mode * eglot.el (eglot--managed-mode): don't enable flymake or eldoc when those symbols belong to eglot-stay-out-of. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/671 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index db468d83c14..13d9952494a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1500,8 +1500,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (unless (eglot--stay-out-of-p 'imenu) (add-function :before-until (local 'imenu-create-index-function) #'eglot-imenu)) - (flymake-mode 1) - (eldoc-mode 1) + (unless (eglot--stay-out-of-p 'flymake) (flymake-mode 1)) + (unless (eglot--stay-out-of-p 'eldoc) (eldoc-mode 1)) (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server)))) (t (remove-hook 'after-change-functions 'eglot--after-change t) From 712cf71d9d1b05ea699f6a1a172dc8eefe1efbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 22 May 2021 11:49:47 +0100 Subject: [PATCH 594/771] Support multiple servers out-of-box for same mode Also per https://github.com/joaotavora/eglot/issues/537. * eglot.el (eglot-alternatives): new helper. (eglot-server-programs): Use it. Use clangd and pylsp. * NEWS.md: Mention feature. * README.md (Connecting to a server): Mention pylsp and clangd. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/688 --- lisp/progmodes/eglot.el | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 13d9952494a..a739419e9dc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -95,15 +95,37 @@ :prefix "eglot-" :group 'applications) -(defvar eglot-server-programs '((rust-mode . (eglot-rls "rls")) - (python-mode . ("pyls")) +(defun eglot-alternatives (alternatives) + "Compute server-choosing function for `eglot-server-programs'. +Each element of ALTERNATIVES is a string PROGRAM or a list of +strings (PROGRAM ARGS...) where program names an LSP server +program to start with ARGS. Returns a function of one +argument." + (lambda (&optional interactive) + (let* ((listified (cl-loop for a in alternatives + collect (if (listp a) a (list a)))) + (available (cl-remove-if-not #'executable-find listified :key #'car))) + (cond ((and interactive (cdr available)) + (let ((chosen (completing-read + "[eglot] More than one server executable available:" + (mapcar #'car available) + nil t nil nil (car (car available))))) + (assoc chosen available #'equal))) + ((car available)) + (t + (car listified)))))) + +(defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) + (python-mode + . ,(eglot-alternatives '("pyls" "pylsp"))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) ((php-mode phps-mode) . ("php" "vendor/felixfbecker/\ language-server/bin/php-language-server.php")) - ((c++-mode c-mode) . ("ccls")) + ((c++-mode c-mode) . ,(eglot-alternatives + '("clangd" "ccls"))) (((caml-mode :language-id "ocaml") (tuareg-mode :language-id "ocaml") reason-mode) . ("ocamllsp")) From 78e994d85517ab0d0f2c053b5e6c3598bdfe7718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 26 May 2021 15:21:06 +0100 Subject: [PATCH 595/771] Again speed up directory watching Previously, given a number of globs, Eglot would try to place system watchers only in those subdirectories that could potentially be matched by a glob. This meant traversing the whole tree, which could be impractical. Just place watchers in every subdirectory of the project (you may run out of watchers). * eglot.el (eglot-register-capability): Simplify. (eglot--files-recursively): Delete. (eglot--directories-recursively): Fix. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/697 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/645 --- lisp/progmodes/eglot.el | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a739419e9dc..706fa92522e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2777,7 +2777,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (eglot--glob-compile globPattern t t)) watchers)) (dirs-to-watch - (eglot--directories-matched-by-globs default-directory globs))) + (eglot--directories-recursively default-directory))) (cl-labels ((handle-event (event) @@ -2878,37 +2878,14 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) -(defun eglot--files-recursively (&optional dir) - "Because `directory-files-recursively' isn't complete in 26.3." - (cons (setq dir (expand-file-name (or dir default-directory))) - (cl-loop with default-directory = dir - with completion-regexp-list = '("^[^.]") - for f in (file-name-all-completions "" dir) - if (file-directory-p f) append (eglot--files-recursively f) - else collect (expand-file-name f)))) - (defun eglot--directories-recursively (&optional dir) "Because `directory-files-recursively' isn't complete in 26.3." (cons (setq dir (expand-file-name (or dir default-directory))) (cl-loop with default-directory = dir with completion-regexp-list = '("^[^.]") for f in (file-name-all-completions "" dir) - if (file-directory-p f) append (eglot--files-recursively f) - else collect (expand-file-name f)))) - -(defun eglot--directories-matched-by-globs (dir globs) - "Discover subdirectories of DIR with files matched by one of GLOBS. -Each element of GLOBS is either an uncompiled glob-string or a -compiled glob." - (setq globs (cl-loop for g in globs - collect (if (stringp g) (eglot--glob-compile g t t) g))) - (cl-loop for f in (eglot--files-recursively dir) - for fdir = (file-name-directory f) - when (and - (not (member fdir dirs)) - (cl-loop for g in globs thereis (funcall g f))) - collect fdir into dirs - finally (cl-return (delete-dups dirs)))) + if (file-directory-p f) + append (eglot--directories-recursively f)))) ;;; Rust-specific From 02dc70363120beb28e770a872ab3960fb0920fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 26 May 2021 15:23:29 +0100 Subject: [PATCH 596/771] Hard code an exception to "node_modules" directores * eglot.el (eglot--directories-recursively): Fix. GitHub-reference: per https://github.com/joaotavora/eglot/issues/697 GitHub-reference: per https://github.com/joaotavora/eglot/issues/645 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 706fa92522e..4c47ad004bb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2884,7 +2884,7 @@ If NOERROR, return predicate, else erroring function." (cl-loop with default-directory = dir with completion-regexp-list = '("^[^.]") for f in (file-name-all-completions "" dir) - if (file-directory-p f) + if (and (file-directory-p f) (not (string= "node_modules/" f))) append (eglot--directories-recursively f)))) From b1a379cd774f40f6709fdfbb5c469fc317f22f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 26 May 2021 18:51:30 +0100 Subject: [PATCH 597/771] Use project-files to know which directory watchers to skip The directory-finding logic is probably a bit slower than using eglot--directories-recursively, but since it honours `.gitignores` and ignores more directories it's much faster overall. And guaranteed to create less watchers. Thanks to Dmitry Gutov for the idea. * eglot.el (eglot--directories-recursively): Remove. GitHub-reference: per https://github.com/joaotavora/eglot/issues/697 --- lisp/progmodes/eglot.el | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4c47ad004bb..4ead874eec5 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2777,7 +2777,9 @@ at point. With prefix argument, prompt for ACTION-KIND." (eglot--glob-compile globPattern t t)) watchers)) (dirs-to-watch - (eglot--directories-recursively default-directory))) + (delete-dups (mapcar #'file-name-directory + (project-files + (eglot--project server)))))) (cl-labels ((handle-event (event) @@ -2878,15 +2880,6 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) -(defun eglot--directories-recursively (&optional dir) - "Because `directory-files-recursively' isn't complete in 26.3." - (cons (setq dir (expand-file-name (or dir default-directory))) - (cl-loop with default-directory = dir - with completion-regexp-list = '("^[^.]") - for f in (file-name-all-completions "" dir) - if (and (file-directory-p f) (not (string= "node_modules/" f))) - append (eglot--directories-recursively f)))) - ;;; Rust-specific ;;; From 7d1375df484e10668552ba23cea0778bf9e374c4 Mon Sep 17 00:00:00 2001 From: Liu Hui Date: Sat, 12 Jun 2021 06:49:19 +0800 Subject: [PATCH 598/771] Consider tramp in eglot-alternatives * eglot.el (eglot-alternatives): Use eglot--executable-find. Copyright-paperwork-exempt: yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/702 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4ead874eec5..a6c60e95781 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -104,7 +104,8 @@ argument." (lambda (&optional interactive) (let* ((listified (cl-loop for a in alternatives collect (if (listp a) a (list a)))) - (available (cl-remove-if-not #'executable-find listified :key #'car))) + (available (cl-remove-if-not (lambda (a) (eglot--executable-find a t)) + listified :key #'car))) (cond ((and interactive (cdr available)) (let ((chosen (completing-read "[eglot] More than one server executable available:" From 42508de4f628d022e961a353fec1d81e1b3f52d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 13 Jun 2021 10:55:24 +0100 Subject: [PATCH 599/771] Don't call eglot--executable-find more than needed * eglot.el (eglot-alternatives): Complexify. (eglot--guess-contact): No need to 'executable-find' if path absolute. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/703 --- lisp/progmodes/eglot.el | 48 ++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a6c60e95781..927009a8a9f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -99,22 +99,42 @@ "Compute server-choosing function for `eglot-server-programs'. Each element of ALTERNATIVES is a string PROGRAM or a list of strings (PROGRAM ARGS...) where program names an LSP server -program to start with ARGS. Returns a function of one -argument." +program to start with ARGS. Returns a function of one argument. +When invoked, that function will return a list (ABSPATH ARGS), +where ABSPATH is the absolute path of the PROGRAM that was +chosen (interactively or automatically)." (lambda (&optional interactive) + ;; JT@2021-06-13: This function is way more complicated than it + ;; could be because it accounts for the fact that + ;; `eglot--executable-find' may take much longer to execute on + ;; remote files. (let* ((listified (cl-loop for a in alternatives collect (if (listp a) a (list a)))) - (available (cl-remove-if-not (lambda (a) (eglot--executable-find a t)) - listified :key #'car))) - (cond ((and interactive (cdr available)) - (let ((chosen (completing-read - "[eglot] More than one server executable available:" - (mapcar #'car available) - nil t nil nil (car (car available))))) - (assoc chosen available #'equal))) - ((car available)) + (err (lambda () + (error "None of '%s' are valid executables" + (mapconcat #'identity alternatives ", "))))) + (cond (interactive + (let* ((augmented (mapcar (lambda (a) + (let ((found (eglot--executable-find + (car a) t))) + (and found + (cons (car a) (cons found (cdr a)))))) + listified)) + (available (remove nil augmented))) + (cond ((cdr available) + (cdr (assoc + (completing-read + "[eglot] More than one server executable available:" + (mapcar #'car available) + nil t nil nil (car (car available))) + available #'equal))) + ((cdr (car available))) + (t (funcall err))))) (t - (car listified)))))) + (cl-loop for (p . args) in listified + for probe = (eglot--executable-find p t) + when probe return (cons probe args) + finally (funcall err))))))) (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) (python-mode @@ -833,7 +853,9 @@ be guessed." ((null guess) (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" managed-mode base-prompt)) - ((and program (not (eglot--executable-find program t))) + ((and program + (not (file-name-absolute-p program)) + (not (eglot--executable-find program t))) (concat (format "[eglot] I guess you want to run `%s'" program-guess) (format ", but I can't find `%s' in PATH!" program) From f07df485c678d8318e896fea9d549d5742d01898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 13 Jun 2021 23:07:42 +0100 Subject: [PATCH 600/771] Transpose order of "pylsp" and "pyls" alternatives When operating remotely, searching for an executable that don't exist takes longer than usual. Better to put the most likely server first in the list to minimize the slowdown. * eglot.el (eglot-server-programs): Transpose python mode alternatives GitHub-reference: per https://github.com/joaotavora/eglot/issues/703 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 927009a8a9f..83bd1024a00 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -138,7 +138,7 @@ chosen (interactively or automatically)." (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) (python-mode - . ,(eglot-alternatives '("pyls" "pylsp"))) + . ,(eglot-alternatives '("pylsp" "pyls"))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) From 7eb81031cd25562c7c566a34cce616fd0410e51d Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Sat, 30 Jan 2021 17:33:08 -0800 Subject: [PATCH 601/771] Add support for locationlink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix https://github.com/joaotavora/eglot/issues/711. LocationLink was added in version 3.14 of the protocol and is sometimes used in lieu of Location for definition- and reference-related requests. * eglot.el (eglot--lsp-interface-alist): Update with LocationLink. (eglot-client-capabilities): Advertise textDocument.{definition,declaration,implementation,typeDefinition}.linkSupport. (eglot--lsp-xrefs-for-method): Accept LocationLinks. Co-authored-by: João Távora Date: Tue, 10 Aug 2021 20:28:35 +0100 Subject: [PATCH 602/771] Let eglot-flymake-backend be in flymake-d-functions even if eglot off This is useful when using eglot-stay-out-of and a pattern like: (defun my/js-mode-hook () (add-hook 'flymake-diagnostic-functions 'some-eslint-backend nil t)) (setq-local eglot-stay-out-of '(flymake)) (add-hook 'flymake-diagnostic-functions 'eglot-flymake-backend nil t)) (add-hook 'js-mode-hook 'my/js-mode-hook) Then, _both_ backends will run unconditionally, but Eglot backend only actually reports diagnostics if Eglot is on. * eglot.el (eglot-flymake-backend): If buffer isn't being managed by Eglot, behave as a noop. --- lisp/progmodes/eglot.el | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0f367fd2208..71f7d7ea597 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2089,10 +2089,13 @@ Calls REPORT-FN (or arranges for it to be called) when the server publishes diagnostics. Between calls to this function, REPORT-FN may be called multiple times (respecting the protocol of `flymake-backend-functions')." - (setq eglot--current-flymake-report-fn report-fn) - ;; Report anything unreported - (when eglot--unreported-diagnostics - (eglot--report-to-flymake (cdr eglot--unreported-diagnostics)))) + (cond (eglot--managed-mode + (setq eglot--current-flymake-report-fn report-fn) + ;; Report anything unreported + (when eglot--unreported-diagnostics + (eglot--report-to-flymake (cdr eglot--unreported-diagnostics)))) + (t + (funcall report-fn nil)))) (defun eglot--report-to-flymake (diags) "Internal helper for `eglot-flymake-backend'." From 82c3a2eff7c990331edb3dc0281751230edd88f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 17 Aug 2021 10:12:27 +0100 Subject: [PATCH 603/771] Fall back to prompting user if eglot-alternatives fails * eglot.el (eglot-alternatives): Don't error in interactive case. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/719 --- lisp/progmodes/eglot.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 71f7d7ea597..4667526c371 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -129,7 +129,10 @@ chosen (interactively or automatically)." nil t nil nil (car (car available))) available #'equal))) ((cdr (car available))) - (t (funcall err))))) + (t + ;; Don't error when used interactively, let the + ;; Eglot prompt the user for alternative (github#719) + nil)))) (t (cl-loop for (p . args) in listified for probe = (eglot--executable-find p t) From 64ffc80e6fb3f3e77dc55149b24e9e7225ceea74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 1 Sep 2021 10:17:24 +0100 Subject: [PATCH 604/771] Fix typo in user-visible eglot-ignored-server-capabilities The name with the typo, eglot-ignored-server-capabilites, is still supported. Per https://github.com/joaotavora/eglot/issues/724. * NEWS.md: Mention change * eglot.el (eglot-ignored-server-capabilities): New defcustom. --- lisp/progmodes/eglot.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4667526c371..5fdc25327f4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1386,7 +1386,10 @@ Doubles as an indicator of snippet support." (font-lock-ensure) (string-trim (filter-buffer-substring (point-min) (point-max)))))) -(defcustom eglot-ignored-server-capabilites (list) +(define-obsolete-variable-alias 'eglot-ignored-server-capabilites + 'eglot-ignored-server-capabilities "1.8") + +(defcustom eglot-ignored-server-capabilities (list) "LSP server capabilities that Eglot could use, but won't. You could add, for instance, the symbol `:documentHighlightProvider' to prevent automatic highlighting From 67fe1c1ad5a1277f4715ef503f14e8a73a972f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 5 Sep 2021 09:44:27 +0100 Subject: [PATCH 605/771] Respect completion-regexp-alist in eglot's completion table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See GitHub discussion https://github.com/joaotavora/eglot/issues/726 Suggested-by: Felicián Németh Suggested-by: JD Smith * eglot (eglot-completion-at-point): use all-completions. --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5fdc25327f4..e8b7ffbbb13 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2363,7 +2363,9 @@ is not active." ((null action) ; try-completion (try-completion probe (funcall proxies))) ((eq action t) ; all-completions - (cl-remove-if-not + (all-completions + "" + (funcall proxies) (lambda (proxy) (let* ((item (get-text-property 0 'eglot--lsp-item proxy)) (filterText (plist-get item :filterText))) From d7057441b823a42b1378eadb0b4488d6fb7069f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 5 Sep 2021 20:05:45 +0100 Subject: [PATCH 606/771] Fixup last commit Per https://github.com/joaotavora/eglot/issues/726. I'm still not entirely convinced using all-completion here is a good idea. As usual the completion list we get from the server is pre-filtered to whatever the server wishes. Letting the completion style do its own filtering (most completion styles use completion-regexp-list and all-completions themselves) is completely useless here. Let's hope it's not harmful. * eglot.el (eglot-completion-at-point): Fix all-completions call --- lisp/progmodes/eglot.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e8b7ffbbb13..7ebc4224519 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2371,8 +2371,7 @@ is not active." (filterText (plist-get item :filterText))) (and (or (null pred) (funcall pred proxy)) (string-prefix-p - probe (or filterText proxy) completion-ignore-case)))) - (funcall proxies))))) + probe (or filterText proxy) completion-ignore-case)))))))) :annotation-function (lambda (proxy) (eglot--dbind ((CompletionItem) detail kind) From c0b74d0b5938583db829363ebafdd9e0701554a4 Mon Sep 17 00:00:00 2001 From: Ingo Lohmar Date: Sat, 9 Oct 2021 21:19:37 +0200 Subject: [PATCH 607/771] Fix workspace/configuration handling when given scopeuri directory The path returned by eglot--uri-to-path is mostly used for file paths, and therefore does not end with a slash. Such a no-trailing-slash path violates what default-directory demands (per its docstring), which causes hack-dir-local-variables-non-file-buffer to not find the appropriate dir-local vars. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7ebc4224519..bf9cf25c33b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2007,7 +2007,7 @@ When called interactively, use the currently active server" (default-directory (if (and (not (string-empty-p uri-path)) (file-directory-p uri-path)) - uri-path + (file-name-as-directory uri-path) (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) From 19d8085b762161ef4e54997acf43b87a535accb4 Mon Sep 17 00:00:00 2001 From: Stephen Leake Date: Sat, 13 Nov 2021 02:39:59 -0800 Subject: [PATCH 608/771] Fix issues; severity not set in textdocument/publishdiagnostics * eglot.el (eglot-handle-notification): Handle severity not set. GitHub-reference: https://github.com/joaotavora/eglot/issues/755 GitHub-reference: https://github.com/joaotavora/eglot/issues/401 --- lisp/progmodes/eglot.el | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bf9cf25c33b..d8890209129 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,6 +1,6 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2018-2020 Free Software Foundation, Inc. +;; Copyright (C) 2018-2021 Free Software Foundation, Inc. ;; Version: 1.7 ;; Author: João Távora @@ -1603,7 +1603,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") :key #'eglot--major-mode) (and eglot-extend-to-xref buffer-file-name - (gethash (expand-file-name buffer-file-name) + (gethash (expand-file-name buffer-file-name) eglot--servers-by-xrefed-file))))) (defun eglot--current-server-or-lose () @@ -1808,7 +1808,8 @@ COMMAND is a symbol naming the command." (point-at-eol (1+ (plist-get (plist-get range :end) :line))))))) (eglot--make-diag (current-buffer) beg end - (cond ((<= sev 1) 'eglot-error) + (cond ((null sev) 'eglot-error) + ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) (t 'eglot-note)) message `((eglot-lsp-diag . ,diag-spec))))) From 907bfe2a93ad172a854979cede87ee0460e33ddd Mon Sep 17 00:00:00 2001 From: Garret Buell Date: Wed, 15 Dec 2021 13:17:26 -0800 Subject: [PATCH 609/771] Mark eglot-completion-at-point capf "non-exclusive" Add :exclusive 'no to eglot-completion-at-point results marking it as non-exclusive. This will allow completion to fall back to other less precise completion backends (e.g. dabbrev) if Eglot's returns no results. * eglot.el (eglot-completion-at-point): Set :exclusive to 'no Copyright-paperwork-exempt: Yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/770 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d8890209129..5fbf9a73616 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2415,6 +2415,7 @@ is not active." (regexp-opt (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) (line-beginning-position)))) + :exclusive 'no :exit-function (lambda (proxy status) (when (eq status 'finished) From c12a611e44d3223286fc6190d505ee85813bcc6f Mon Sep 17 00:00:00 2001 From: Fredrik Bergroth Date: Wed, 15 Dec 2021 22:31:20 +0100 Subject: [PATCH 610/771] Add missing entries from completionitemkind * eglot.el (eglot--kind-names): update GitHub-reference: fix https://github.com/joaotavora/eglot/issues/772 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5fbf9a73616..06632fde92d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -310,7 +310,9 @@ let the buffer grow forever." (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") (9 . "Module") (10 . "Property") (11 . "Unit") (12 . "Value") (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") - (17 . "File") (18 . "Reference"))) + (17 . "File") (18 . "Reference") (19 . "Folder") (20 . "EnumMember") + (21 . "Constant") (22 . "Struct") (23 . "Event") (24 . "Operator") + (25 . "TypeParameter"))) (defconst eglot--{} (make-hash-table) "The empty JSON object.") From 426d97f172c34cd10b9aabf0f593f39e24ec502b Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sat, 8 Jan 2022 16:02:06 +0100 Subject: [PATCH 611/771] ; prefer https to http addresses --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 06632fde92d..19091e8c3e7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -20,7 +20,7 @@ ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see . +;; along with this program. If not, see . ;;; Commentary: @@ -2182,7 +2182,7 @@ Try to visit the target file for a richer summary line." ;; JT@19/10/09: This is a totally dummy identifier that isn't even ;; passed to LSP. The reason for this particular wording is to ;; construct a readable message "No references for LSP identifier at - ;; point.". See http://github.com/joaotavora/eglot/issues/314 + ;; point.". See https://github.com/joaotavora/eglot/issues/314 "LSP identifier at point.") (defvar eglot--lsp-xref-refs nil From 51fc8fc3d2ea241dc0c7982bb43ed4e1f128f363 Mon Sep 17 00:00:00 2001 From: lorniu/sz Date: Sun, 26 Dec 2021 18:37:14 +0800 Subject: [PATCH 612/771] Use `locate-user-emacs-file` instead of `concat` * eglot.el (eglot--eclipse-jdt-contact): Use locate-user-emacs-file. Copyright-paperwork-exempt: Yes --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 19091e8c3e7..0f31da99844 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3013,8 +3013,8 @@ If INTERACTIVE, prompt user for details." (t "config_linux")))) (workspace (expand-file-name (md5 (project-root (eglot--current-project))) - (concat user-emacs-directory - "eglot-eclipse-jdt-cache")))) + (locate-user-emacs-file + "eglot-eclipse-jdt-cache")))) (unless jar (setq jar (cl-find-if #'is-the-jar From b1c7aa1d17adf1da5f1e76482dad7c46ddadcae7 Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Mon, 4 Oct 2021 22:39:12 -0700 Subject: [PATCH 613/771] Add yaml-language-server for yaml-mode * eglot.el (eglot-server-programs): Add yaml-language-server. * README.md: Mention yaml-language-server. --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0f31da99844..a25a143709d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -170,6 +170,7 @@ language-server/bin/php-language-server.php")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) + (yaml-mode . ("yaml-language-server" "--stdio")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) (f90-mode . ("fortls")) From fb8706165c0fed8281d4bc00fac39d86f89c1036 Mon Sep 17 00:00:00 2001 From: Philipp Edelmann Date: Wed, 13 May 2020 11:50:12 -0600 Subject: [PATCH 614/771] Use fortls also for fortran-mode * eglot.el (eglot-server-programs): Use fortls also for fortran-mode. Copyright-paperwork-exempt: Yes --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a25a143709d..2e9531cffb4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -173,7 +173,7 @@ language-server/bin/php-language-server.php")) (yaml-mode . ("yaml-language-server" "--stdio")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) - (f90-mode . ("fortls")) + ((fortran-mode f90-mode) . ("fortls")) (zig-mode . ("zls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE From 3967d22b9c8b00833ee9b051017b613a788d6217 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sat, 8 Jan 2022 18:18:16 +0100 Subject: [PATCH 615/771] ; fix typos --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e9531cffb4..abcd0781e16 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -50,7 +50,7 @@ ;; ;; (add-hook 'foo-mode-hook 'eglot-ensure) ;; -;; To attempt to start an eglot session automatically everytime a +;; To attempt to start an eglot session automatically every time a ;; foo-mode buffer is visited. ;; ;;; Code: @@ -1490,7 +1490,7 @@ against a variable's name. Examples include the string \"company\" or the symbol `xref'. Before Eglot starts \"managing\" a particular buffer, it -opinionatedly sets some peripheral Emacs facilites, such as +opinionatedly sets some peripheral Emacs facilities, such as Flymake, Xref and Company. These overriding settings help ensure consistent Eglot behaviour and only stay in place until \"managing\" stops (usually via `eglot-shutdown'), whereupon the @@ -1695,7 +1695,7 @@ Uses THING, FACE, DEFS and PREPEND." `("/" ,(eglot--mode-line-props "error" 'compilation-mode-line-fail '((mouse-3 eglot-clear-status "clear this status")) - (format "An error occured: %s\n" (plist-get last-error + (format "An error occurred: %s\n" (plist-get last-error :message))))) ,@(when (and doing (not done-p)) `("/" ,(eglot--mode-line-props doing From bbdfbf0456bcf2fd55b78ea18e8785e64a5a8793 Mon Sep 17 00:00:00 2001 From: Illia Danko Date: Sat, 18 Sep 2021 15:13:39 +0300 Subject: [PATCH 616/771] Add pyright language server support for python-mode * eglot.el (eglot-server-programs): Add pyright support for python-mode. * README.md: Document the above change. Copyright-paperwork-exempt: Yes --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index abcd0781e16..43b27af90f0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -141,7 +141,8 @@ chosen (interactively or automatically)." (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) (python-mode - . ,(eglot-alternatives '("pylsp" "pyls"))) + . ,(eglot-alternatives + '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) From cf0ba0197a965afd2aca5798ed990a41f2124585 Mon Sep 17 00:00:00 2001 From: jgart <47760695+jgarte@users.noreply.github.com> Date: Sat, 2 Oct 2021 00:20:57 -0400 Subject: [PATCH 617/771] Add support for the mint language server * eglot.el (eglot-server-programs): Add support for the mint language server. * README.md: Document the above change. Copyright-paperwork-exempt: Yes --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 43b27af90f0..725fb6c27d1 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -159,6 +159,7 @@ language-server/bin/php-language-server.php")) (haskell-mode . ("haskell-language-server-wrapper" "--lsp")) (elm-mode . ("elm-language-server")) + (mint-mode . ("mint" "ls")) (kotlin-mode . ("kotlin-language-server")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" From 06f1cd6365916e54b69b3a7fde3eb23c8a54b00b Mon Sep 17 00:00:00 2001 From: Omar Polo Date: Mon, 30 Aug 2021 12:32:32 +0000 Subject: [PATCH 618/771] Add lua-lsp support for lua-mode * eglot.el (eglot-server-programs): Add support for the lua-lsp server for lua. * README.md: Document the above change. --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 725fb6c27d1..e9f48bb3c30 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -176,6 +176,7 @@ language-server/bin/php-language-server.php")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) ((fortran-mode f90-mode) . ("fortls")) + (lua-mode . ("lua-lsp")) (zig-mode . ("zls"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE From 7c712abf8f8e6f9a85b80339c00316c35b4beb14 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sat, 8 Jan 2022 19:43:29 +0100 Subject: [PATCH 619/771] Minor checkdoc fixes * eglot.el (eglot, eglot--when-buffer-window, eglot--widening) (eglot-initialization-options, eglot--current-flymake-report-fn) (eglot-handle-notification, eglot-handle-request) (eglot--highlight-piggyback, eglot-register-capability) (eglot-unregister-capability): * eglot-tests.el (auto-detect-running-server, auto-shutdown) (auto-reconnect, eglot--tests-force-full-eldoc, rename-a-symbol) (basic-completions, non-unique-completions, basic-xref) (snippet-completions, snippet-completions-with-company) (eglot-eldoc-after-completions, python-yapf-formatting) (javascript-basic, json-basic, eglot-ensure) (eglot--guessing-contact): Doc fixes; formatting. * eglot.el (xref-backend-identifier-completion-table): Fix error format. --- lisp/progmodes/eglot.el | 42 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e9f48bb3c30..234e4f14e77 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -91,7 +91,7 @@ ;;; User tweakable stuff (defgroup eglot nil - "Interaction with Language Server Protocol servers" + "Interaction with Language Server Protocol servers." :prefix "eglot-" :group 'applications) @@ -577,7 +577,7 @@ treated as in `eglot-dbind'." `(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)) + "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' @@ -585,7 +585,7 @@ treated as in `eglot-dbind'." (with-current-buffer ,b ,@body))))) (cl-defmacro eglot--widening (&rest body) - "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) + "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) `(save-excursion (save-restriction (widen) ,@body))) (cl-defgeneric eglot-handle-request (server method &rest params) @@ -598,7 +598,7 @@ treated as in `eglot-dbind'." "Ask SERVER to execute COMMAND with ARGUMENTS.") (cl-defgeneric eglot-initialization-options (server) - "JSON object to send under `initializationOptions'" + "JSON object to send under `initializationOptions'." (:method (_s) eglot--{})) ; blank default (cl-defgeneric eglot-register-capability (server method id &rest params) @@ -1481,7 +1481,7 @@ and just return it. PROMPT shouldn't end with a question mark." map)) (defvar-local eglot--current-flymake-report-fn nil - "Current flymake report function for this buffer") + "Current flymake report function for this buffer.") (defvar-local eglot--saved-bindings nil "Bindings saved by `eglot--setq-saving'.") @@ -1734,14 +1734,14 @@ Uses THING, FACE, DEFS and PREPEND." ;;; (cl-defmethod eglot-handle-notification (_server method &key &allow-other-keys) - "Handle unknown notification" + "Handle unknown notification." (unless (or (string-prefix-p "$" (format "%s" method)) (not (memq 'disallow-unknown-methods eglot-strict-mode))) (eglot--warn "Server sent unknown notification method `%s'" method))) (cl-defmethod eglot-handle-request (_server method &key &allow-other-keys) - "Handle unknown request" + "Handle unknown request." (when (memq 'disallow-unknown-methods eglot-strict-mode) (jsonrpc-error "Unknown request method `%s'" method))) @@ -1754,14 +1754,14 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-notification (_server (_method (eql window/showMessage)) &key type message) - "Handle notification window/showMessage" + "Handle notification window/showMessage." (eglot--message (propertize "Server reports (type=%s): %s" 'face (if (<= type 1) 'error)) type message)) (cl-defmethod eglot-handle-request (_server (_method (eql window/showMessageRequest)) &key type message actions) - "Handle server request window/showMessageRequest" + "Handle server request window/showMessageRequest." (let* ((actions (append actions nil)) ;; gh#627 (label (completing-read (concat @@ -1776,16 +1776,16 @@ COMMAND is a symbol naming the command." (cl-defmethod eglot-handle-notification (_server (_method (eql window/logMessage)) &key _type _message) - "Handle notification window/logMessage") ;; noop, use events buffer + "Handle notification window/logMessage.") ;; noop, use events buffer (cl-defmethod eglot-handle-notification (_server (_method (eql telemetry/event)) &rest _any) - "Handle notification telemetry/event") ;; noop, use events buffer + "Handle notification telemetry/event.") ;; noop, use events buffer (cl-defmethod eglot-handle-notification (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' - "Handle notification publishDiagnostics" + "Handle notification publishDiagnostics." (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer (cl-loop @@ -1839,18 +1839,18 @@ THINGS are either registrations or unregisterations (sic)." (cl-defmethod eglot-handle-request (server (_method (eql client/registerCapability)) &key registrations) - "Handle server request client/registerCapability" + "Handle server request client/registerCapability." (eglot--register-unregister server registrations 'register)) (cl-defmethod eglot-handle-request (server (_method (eql client/unregisterCapability)) &key unregisterations) ;; XXX: "unregisterations" (sic) - "Handle server request client/unregisterCapability" + "Handle server request client/unregisterCapability." (eglot--register-unregister server unregisterations 'unregister)) (cl-defmethod eglot-handle-request (_server (_method (eql workspace/applyEdit)) &key _label edit) - "Handle server request workspace/applyEdit" + "Handle server request workspace/applyEdit." (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) (defun eglot--TextDocumentIdentifier () @@ -2180,7 +2180,7 @@ Try to visit the target file for a richer summary line." (xref-make-match summary (xref-make-file-location file line column) length))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (eglot--error "cannot (yet) provide reliable completion table for LSP symbols")) + (eglot--error "Cannot (yet) provide reliable completion table for LSP symbols")) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) ;; JT@19/10/09: This is a totally dummy identifier that isn't even @@ -2578,7 +2578,7 @@ is not active." (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") (defun eglot--highlight-piggyback (_cb) - "Request and handle `:textDocument/documentHighlight'" + "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))) @@ -2821,7 +2821,7 @@ at point. With prefix argument, prompt for ACTION-KIND." ;;; (cl-defmethod eglot-register-capability (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) - "Handle dynamic registration of workspace/didChangeWatchedFiles" + "Handle dynamic registration of workspace/didChangeWatchedFiles." (eglot-unregister-capability server method id) (let* (success (globs (mapcar @@ -2863,7 +2863,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (cl-defmethod eglot-unregister-capability (server (_method (eql workspace/didChangeWatchedFiles)) id) - "Handle dynamic unregistration of workspace/didChangeWatchedFiles" + "Handle dynamic unregistration of workspace/didChangeWatchedFiles." (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches server))) (remhash id (eglot--file-watches server)) (list t "OK")) @@ -2948,7 +2948,7 @@ If NOERROR, return predicate, else erroring function." (cl-defmethod eglot-handle-notification ((server eglot-rls) (_method (eql window/progress)) &key id done title message &allow-other-keys) - "Handle notification window/progress" + "Handle notification window/progress." (setf (eglot--spinner server) (list id title done message))) @@ -2958,7 +2958,7 @@ If NOERROR, return predicate, else erroring function." :documentation "Eclipse's Java Development Tools Language Server.") (cl-defmethod eglot-initialization-options ((server eglot-eclipse-jdt)) - "Passes through required jdt initialization options" + "Passes through required jdt initialization options." `(:workspaceFolders [,@(cl-delete-duplicates (mapcar #'eglot--path-to-uri From a35f6a7f9abe2e67ec76e932bdb9f4424cd12fc2 Mon Sep 17 00:00:00 2001 From: NA Date: Fri, 11 Jan 2019 05:58:04 +0200 Subject: [PATCH 620/771] Support language server for html, css, json and docker * eglot.el (eglot-server-programs): Support html-languageserver, css-languageserver, json-languageserver, and docker-langserver. * README.md: Update documentation for above changes. Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/204 --- lisp/progmodes/eglot.el | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 234e4f14e77..e1eb9ed4ced 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -177,7 +177,11 @@ language-server/bin/php-language-server.php")) (gdscript-mode . ("localhost" 6008)) ((fortran-mode f90-mode) . ("fortls")) (lua-mode . ("lua-lsp")) - (zig-mode . ("zls"))) + (zig-mode . ("zls")) + (css-mode "css-languageserver" "--stdio") + (html-mode "html-languageserver" "--stdio") + (json-mode "json-languageserver" "--stdio") + (dockerfile-mode . ("docker-langserver" "--stdio"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific From 512d8b9f590df3b553526bffb7b39ee4f0cd76e9 Mon Sep 17 00:00:00 2001 From: Martin Carlson Date: Tue, 31 Aug 2021 12:24:34 +0200 Subject: [PATCH 621/771] Add variable to withhold the init req process id * eglot.el (eglot-withhold-process-id): New defvar. (eglot--connect): Don't send pid to language server if above new defvar has a non-nil value. Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/722 --- lisp/progmodes/eglot.el | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e1eb9ed4ced..712ad172985 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -295,6 +295,10 @@ let the buffer grow forever." "If non-nil, activate Eglot in cross-referenced non-project files." :type 'boolean) +(defvar eglot-withhold-process-id nil + "If non-nil, Eglot will not send the Emacs process id to the language server. +This can be useful when using docker to run a language server.") + ;; Customizable via `completion-category-overrides'. (when (assoc 'flex completion-styles-alist) (add-to-list 'completion-category-defaults '(eglot (styles flex basic)))) @@ -1110,7 +1114,8 @@ This docstring appeases checkdoc, that's all." server :initialize (list :processId - (unless (or (file-remote-p default-directory) + (unless (or eglot-withhold-process-id + (file-remote-p default-directory) (eq (jsonrpc-process-type server) 'network)) (emacs-pid)) From a218f52ec9b0324b7026ae449067167ab833d503 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sun, 9 Jan 2022 02:50:38 +0100 Subject: [PATCH 622/771] Un-reverse references in xref buffer GitHub-reference: fix https://github.com/joaotavora/eglot/issues/763 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 712ad172985..72cf0c44fcb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2148,6 +2148,7 @@ may be called multiple times (respecting the protocol of (let (,collected) (cl-flet ((,collector (xref) (push xref ,collected))) ,@body) + (setq ,collected (nreverse ,collected)) (sort ,collected eglot-xref-lessp-function)) (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) (clrhash eglot--temp-location-buffers)))) From be1e214fb2911e4563298e5542bfa98dc23bf332 Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Sat, 8 Jan 2022 18:27:29 -0800 Subject: [PATCH 623/771] Add cmake-language-server for cmake-mode * README.md: Advertise. * eglot.el (eglot-server-programs): Add cmake-language-server. --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 72cf0c44fcb..3cd006d1698 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -140,6 +140,7 @@ chosen (interactively or automatically)." finally (funcall err))))))) (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) + (cmake-mode . ("cmake-language-server")) (python-mode . ,(eglot-alternatives '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) From a8b3b6a5263798c990a5f5c46a074b51355513cd Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Sat, 8 Jan 2022 18:30:57 -0800 Subject: [PATCH 624/771] Add vim-language-server for vimrc-mode * README.md: Advertise. * eglot.el (eglot-server-programs): Add vim-language-server. --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3cd006d1698..bf2e9c0fd94 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -141,6 +141,7 @@ chosen (interactively or automatically)." (defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) (cmake-mode . ("cmake-language-server")) + (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode . ,(eglot-alternatives '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) From eacc40e3bfcf316a52b48185383262821a17820d Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sun, 9 Jan 2022 17:38:40 +0100 Subject: [PATCH 625/771] ; update copyright years --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bf2e9c0fd94..8163b7f9184 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,6 +1,6 @@ ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- -;; Copyright (C) 2018-2021 Free Software Foundation, Inc. +;; Copyright (C) 2018-2022 Free Software Foundation, Inc. ;; Version: 1.7 ;; Author: João Távora From ae7315b5f11dc22ef0ebaa1f9ce1546d2ba34ab8 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sun, 9 Jan 2022 17:41:40 +0100 Subject: [PATCH 626/771] ; fix license statement Packages in GNU ELPA are considered part of GNU Emacs. --- lisp/progmodes/eglot.el | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8163b7f9184..2a8000cce3f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -9,23 +9,25 @@ ;; Keywords: convenience, languages ;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0")) -;; This program is free software; you can redistribute it and/or modify +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. -;; This program is distributed in the hope that it will be useful, +;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see . +;; along with GNU Emacs. If not, see . ;;; Commentary: ;; Simply M-x eglot should be enough to get you started, but here's a - ;; little info (see the accompanying README.md or the URL for more). +;; little info (see the accompanying README.md or the URL for more). ;; ;; M-x eglot starts a server via a shell-command guessed from ;; `eglot-server-programs', using the current major-mode (for whatever From 49e46c3d5333b0aa10f0eeefba03f95b8a4a9862 Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Sat, 8 Jan 2022 17:43:23 -0800 Subject: [PATCH 627/771] Add up-to-date server executables for html/css/json * README.md: Advertise updated executables. * eglot.el (eglot-server-programs): Prioritize the alternatives. The {html,css,json}-languageserver executables that are distributed outside VS Code are not regularly updated by Microsoft; any relevant updates to the VS Code source tree reach VS Code users without the need for VS Code developers to go out of their way to publish new versions of the executables. Consequently, users of other editors who have been using the server executables from the most obvious NPM packages are likely using stale versions. @hrsh7th, a Vim user, created an NPM package with updated versions of these executables taken straight from VS Code's source tree. We therefore prefer to direct users to the corresponding repo, which contains appropriate installation instructions, in the README. --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2a8000cce3f..ef9b371af89 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -182,9 +182,9 @@ language-server/bin/php-language-server.php")) ((fortran-mode f90-mode) . ("fortls")) (lua-mode . ("lua-lsp")) (zig-mode . ("zls")) - (css-mode "css-languageserver" "--stdio") - (html-mode "html-languageserver" "--stdio") - (json-mode "json-languageserver" "--stdio") + (css-mode . ,(eglot-alternatives '(("vscode-css-language-server" "--stdio") ("css-languageserver" "--stdio")))) + (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) + (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) (dockerfile-mode . ("docker-langserver" "--stdio"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE From f0b9018f521292d970cdaed99c47e02b60026e52 Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Sat, 8 Jan 2022 18:08:23 -0800 Subject: [PATCH 628/771] Properly print error message of eglot-alternatives * eglot.el (eglot-alternatives): Work with the listified form. This allows presumed executables provided as (EXECUTABLE &rest ARGS...) to be displayed in the error. GitHub-reference: per https://github.com/joaotavora/eglot/issues/786 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ef9b371af89..922b9225d27 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -114,7 +114,7 @@ chosen (interactively or automatically)." collect (if (listp a) a (list a)))) (err (lambda () (error "None of '%s' are valid executables" - (mapconcat #'identity alternatives ", "))))) + (mapconcat #'car listified ", "))))) (cond (interactive (let* ((augmented (mapcar (lambda (a) (let ((found (eglot--executable-find From 09c071d3d1feee5c50afb2ba3e80b4f5c79ca793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Mon, 10 Jan 2022 20:21:21 +0100 Subject: [PATCH 629/771] Add tooltip describing pending requests * eglot.el (eglot--mode-line-format): Add tooltip to `pending'. GitHub-reference: per https://github.com/joaotavora/eglot/issues/784 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 922b9225d27..85220f2cd52 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1720,7 +1720,9 @@ Uses THING, FACE, DEFS and PREPEND." `("/" ,(eglot--mode-line-props (format "%d" pending) 'warning '((mouse-3 eglot-forget-pending-continuations - "forget pending continuations")))))))))) + "forget pending continuations")) + "Number of outgoing, \ +still unanswered LSP requests to the server")))))))) (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) From 34c7da506d85d7bb160e60957163eef941804364 Mon Sep 17 00:00:00 2001 From: Fredrik Bergroth Date: Wed, 15 Dec 2021 13:05:22 +0100 Subject: [PATCH 630/771] Support autoimporttext from pyright language server * eglot.el (eglot-completion-at-point): show autoImportText via company-docsig. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/769 --- lisp/progmodes/eglot.el | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 85220f2cd52..80eb58f5791 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2416,6 +2416,13 @@ is not active." (kind (alist-get (plist-get lsp-item :kind) eglot--kind-names))) (intern (downcase kind)))) + :company-docsig + ;; FIXME: autoImportText is specific to the pyright language server + (lambda (proxy) + (when-let* ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy)) + (data (plist-get (funcall resolve-maybe lsp-comp) :data)) + (import-text (plist-get data :autoImportText))) + import-text)) :company-doc-buffer (lambda (proxy) (let* ((documentation From 9adb310e08410b555651d7b819671842bd99f2c1 Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Mon, 10 Jan 2022 19:32:19 -0800 Subject: [PATCH 631/771] Don't error out on unsupported diagnostic.codedescription A codeDescription property is, at the time of writing, an object with an href property (of type URI, or a string), denoting a "URI to open with more information about the diagnostic error". It's not obvious how best to put this into a Flymake diagostic aside from simply appending it to the diagnostic message, so we'll worry about it some other time. * eglot.el (eglot--lsp-interface-alist) (eglot-client-capabilities): Don't error out on unsupported Diagnostic.codeDescription. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/768 --- lisp/progmodes/eglot.el | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 80eb58f5791..bdd0dcccc5e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -351,7 +351,7 @@ This can be useful when using docker to run a language server.") :sortText :filterText :insertText :insertTextFormat :textEdit :additionalTextEdits :commitCharacters :command :data)) - (Diagnostic (:range :message) (:severity :code :source :relatedInformation)) + (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription)) (DocumentHighlight (:range) (:kind)) (FileSystemWatcher (:globPattern) (:kind)) (Hover (:contents) (:range)) @@ -689,7 +689,11 @@ treated as in `eglot-dbind'." :formatting `(:dynamicRegistration :json-false) :rangeFormatting `(:dynamicRegistration :json-false) :rename `(:dynamicRegistration :json-false) - :publishDiagnostics `(:relatedInformation :json-false)) + :publishDiagnostics (list :relatedInformation :json-false + ;; TODO: We can support :codeDescription after + ;; adding an appropriate UI to + ;; Flymake. + :codeDescriptionSupport :json-false)) :experimental eglot--{}))) (defclass eglot-lsp-server (jsonrpc-process-connection) From c06860b0f47e74a22f61eb2a93e6486efdfb11c7 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Wed, 12 Jan 2022 19:35:22 +0100 Subject: [PATCH 632/771] Bump eglot version to 1.8 * eglot.el (Version): Bump to 1.8. * NEWS.md (1.8): Rename header from "(upcoming)". --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bdd0dcccc5e..1d6d298ce8b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018-2022 Free Software Foundation, Inc. -;; Version: 1.7 +;; Version: 1.8 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From b527764963273999c8d466420e18922bc82650e0 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 13 Jan 2022 11:52:29 +0100 Subject: [PATCH 633/771] Support racket-langserver * eglot.el (eglot-server-programs): Support racket-langserver. * README.md: * NEWS.md: Update for above changes. GitHub-reference: per https://github.com/joaotavora/eglot/issues/694 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1d6d298ce8b..4fa2b073b1b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -173,6 +173,7 @@ language-server/bin/php-language-server.php")) (elixir-mode . ("language_server.sh")) (ada-mode . ("ada_language_server")) (scala-mode . ("metals-emacs")) + (racket-mode . ("racket" "-l" "racket-langserver")) ((tex-mode context-mode texinfo-mode bibtex-mode) . ("digestif")) (erlang-mode . ("erlang_ls" "--transport" "stdio")) From a905bad63305b456eeaaa5d716b9f7fcb60ec5ab Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 13 Jan 2022 12:30:17 +0100 Subject: [PATCH 634/771] * eglot.el: improve commentary section. --- lisp/progmodes/eglot.el | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4fa2b073b1b..813b29775e2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -26,35 +26,36 @@ ;;; Commentary: -;; Simply M-x eglot should be enough to get you started, but here's a +;; Eglot ("Emacs Polyglot") is an Emacs LSP client that stays out of +;; your way. +;; +;; Typing M-x eglot should be enough to get you started, but here's a ;; little info (see the accompanying README.md or the URL for more). ;; -;; M-x eglot starts a server via a shell-command guessed from -;; `eglot-server-programs', using the current major-mode (for whatever +;; M-x eglot starts a server via a shell command guessed from +;; `eglot-server-programs', using the current major mode (for whatever ;; language you're programming in) as a hint. If it can't guess, it -;; prompts you in the mini-buffer for these things. Actually, the -;; server needen't be locally started: you can connect to a running -;; server via TCP by entering a syntax. +;; prompts you in the minibuffer for these things. Actually, the +;; server does not need to be running locally: you can connect to a +;; running server via TCP by entering a syntax. ;; -;; Anyway, if the connection is successful, you should see an `eglot' +;; If the connection is successful, you should see an `eglot' ;; indicator pop up in your mode-line. More importantly, this means -;; current *and future* file buffers of that major mode *inside your -;; current project* automatically become \"managed\" by the LSP -;; server, i.e. information about their contents is exchanged -;; periodically to provide enhanced code analysis via +;; that current *and future* file buffers of that major mode *inside +;; your current project* automatically become \"managed\" by the LSP +;; server. In other words, information about their content is +;; exchanged periodically to provide enhanced code analysis using ;; `xref-find-definitions', `flymake-mode', `eldoc-mode', ;; `completion-at-point', among others. ;; -;; To "unmanage" these buffers, shutdown the server with M-x -;; eglot-shutdown. +;; To "unmanage" these buffers, shutdown the server with +;; M-x eglot-shutdown. ;; -;; You can also do: +;; To start an eglot session automatically when a foo-mode buffer is +;; visited, you can put this in your init file: ;; ;; (add-hook 'foo-mode-hook 'eglot-ensure) -;; -;; To attempt to start an eglot session automatically every time a -;; foo-mode buffer is visited. -;; + ;;; Code: (require 'json) From 0f44d338f17bd426ab43f6a619873c9ac91bc51e Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Mon, 10 Jan 2022 21:48:21 -0800 Subject: [PATCH 635/771] Support optional diagnostic.tags https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#diagnosticTag A DiagnosticTag can be either 1 (DiagnosticTag.Unnecessary) or 2 (DiagnosticTag.Deprecated). Following the rendering suggestions in the protocol, we fade out Unnecessary code and strike-through Deprecated code. * eglot.el (eglot-diagnostic-tag-unnecessary-face) (eglot-diagnostic-tag-deprecated-face): New faces. (eglot--tag-faces): New defconst. (eglot--lsp-interface-alist): Add Diagnostic.tags. (eglot-client-capabilities): Advertise supported tags. (eglot-handle-notification): Assign the appropriate properties. * eglot-tests.el (diagnostic-tags-unnecessary-code): New test. GitHub-reference: per https://github.com/joaotavora/eglot/issues/794 --- lisp/progmodes/eglot.el | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 813b29775e2..dc20ef751b8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -252,6 +252,14 @@ CONTACT can be: '((t (:inherit font-lock-constant-face :weight bold))) "Face for package-name in EGLOT's mode line.") +(defface eglot-diagnostic-tag-unnecessary-face + '((t . (:weight ultra-light))) + "Face used to render unused or unnecessary code.") + +(defface eglot-diagnostic-tag-deprecated-face + '((t . (:strike-through t))) + "Face used to render deprecated or obsolete code.") + (defcustom eglot-autoreconnect 3 "Control ability to reconnect automatically to the LSP server. If t, always reconnect automatically (not recommended). If nil, @@ -332,6 +340,10 @@ This can be useful when using docker to run a language server.") (21 . "Constant") (22 . "Struct") (23 . "Event") (24 . "Operator") (25 . "TypeParameter"))) +(defconst eglot--tag-faces + `((1 . eglot-diagnostic-tag-unnecessary-face) + (2 . eglot-diagnostic-tag-deprecated-face))) + (defconst eglot--{} (make-hash-table) "The empty JSON object.") (defun eglot--executable-find (command &optional remote) @@ -353,7 +365,7 @@ This can be useful when using docker to run a language server.") :sortText :filterText :insertText :insertTextFormat :textEdit :additionalTextEdits :commitCharacters :command :data)) - (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription)) + (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription :tags)) (DocumentHighlight (:range) (:kind)) (FileSystemWatcher (:globPattern) (:kind)) (Hover (:contents) (:range)) @@ -695,7 +707,11 @@ treated as in `eglot-dbind'." ;; TODO: We can support :codeDescription after ;; adding an appropriate UI to ;; Flymake. - :codeDescriptionSupport :json-false)) + :codeDescriptionSupport :json-false + :tagSupport + `(:valueSet + [,@(mapcar + #'car eglot--tag-faces)]))) :experimental eglot--{}))) (defclass eglot-lsp-server (jsonrpc-process-connection) @@ -1811,7 +1827,7 @@ COMMAND is a symbol naming the command." (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (eglot--dbind ((Diagnostic) range message severity source) + collect (eglot--dbind ((Diagnostic) range message severity source tags) diag-spec (setq message (concat source ": " message)) (pcase-let @@ -1839,7 +1855,11 @@ COMMAND is a symbol naming the command." ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) (t 'eglot-note)) - message `((eglot-lsp-diag . ,diag-spec))))) + message `((eglot-lsp-diag . ,diag-spec)) + (and tags + `((face . ,(mapcar (lambda (tag) + (alist-get tag eglot--tag-faces)) + tags))))))) into diags finally (cond (eglot--current-flymake-report-fn (eglot--report-to-flymake diags)) From 5b88ec259ccf24b87a7c8c3d35173a2353dd8517 Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Mon, 30 Dec 2019 16:26:22 +0100 Subject: [PATCH 636/771] Change from symbol-at-point to thing-at-point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot-rename): Change from symbol-at-point to thing-at-point to avoid interning a symbol. Add "unknown symbol" to prompt when no symbol is found. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/385 --- lisp/progmodes/eglot.el | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index dc20ef751b8..4e92bf09bf2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2774,9 +2774,11 @@ is not active." (defun eglot-rename (newname) "Rename the current symbol to NEWNAME." (interactive - (list (read-from-minibuffer (format "Rename `%s' to: " (symbol-at-point)) - nil nil nil nil - (symbol-name (symbol-at-point))))) + (list (read-from-minibuffer + (format "Rename `%s' to: " (or (thing-at-point 'symbol t) + "unknown symbol")) + nil nil nil nil + (symbol-name (symbol-at-point))))) (unless (eglot--server-capable :renameProvider) (eglot--error "Server can't rename!")) (eglot--apply-workspace-edit From f199060ee023bc47f0c4260ca88e9072750ec2b7 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 13 Jan 2022 22:47:36 +0100 Subject: [PATCH 637/771] ; * eglot.el (eglot-server-initialized-hook): fix punctuation. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4e92bf09bf2..b97e3a4e668 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1046,7 +1046,7 @@ Use current server's or first available Eglot events buffer." '() "Hook run after a `eglot-lsp-server' instance is created. -That is before a connection was established. Use +That is before a connection was established. Use `eglot-connect-hook' to hook into when a connection was successfully established and the server on the other side has received the initializing configuration. From 26bd153b9087635ca4a820e253ac0fe6ca21df64 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 13 Jan 2022 00:50:35 +0100 Subject: [PATCH 638/771] Print server command to events buffer * eglot.el (eglot--connect): Print server command to events buffer. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/476 --- lisp/progmodes/eglot.el | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b97e3a4e668..9536a72a263 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1078,16 +1078,20 @@ This docstring appeases checkdoc, that's all." (nickname (file-name-base (directory-file-name default-directory))) (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) autostart-inferior-process + server-info (contact (if (functionp contact) (funcall contact) contact)) (initargs (cond ((keywordp (car contact)) contact) ((integerp (cadr contact)) + (setq server-info (list (format "%s:%s" (car contact) + (cadr contact)))) `(:process ,(lambda () (apply #'open-network-stream readable-name nil (car contact) (cadr contact) (cddr contact))))) ((and (stringp (car contact)) (memq :autoport contact)) + (setq server-info (list "")) `(:process ,(lambda () (pcase-let ((`(,connection . ,inferior) (eglot--inferior-bootstrap @@ -1101,7 +1105,7 @@ This docstring appeases checkdoc, that's all." (let ((default-directory default-directory)) (make-process :name readable-name - :command (eglot--cmd contact) + :command (setq server-info (eglot--cmd contact)) :connection-type 'pipe :coding 'utf-8-emacs-unix :noquery t @@ -1122,6 +1126,9 @@ This docstring appeases checkdoc, that's all." initargs)) (cancelled nil) (tag (make-symbol "connected-catch-tag"))) + (when server-info + (jsonrpc--debug server "Running language server: %s" + (string-join server-info " "))) (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) From ed9800041ef2cc05145865134371fa3f0b846497 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sat, 15 Jan 2022 13:47:30 +0100 Subject: [PATCH 639/771] * eglot.el (eglot--connect): display seconds on timeout. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9536a72a263..bf3e54c6b4a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1208,7 +1208,8 @@ in project `%s'." :timeout-fn (lambda () (unless cancelled (jsonrpc-shutdown server) - (let ((msg (format "Timed out"))) + (let ((msg (format "Timed out after %s seconds" + eglot-connect-timeout))) (if tag (throw tag `(error . ,msg)) (eglot--error msg)))))) (cond ((numberp eglot-sync-connect) From 1616da4f26158e66c32e4f9e5f07517a224a451a Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sat, 15 Jan 2022 17:35:19 +0100 Subject: [PATCH 640/771] * eglot.el (eglot-strict-mode): very minor docfix. --- lisp/progmodes/eglot.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bf3e54c6b4a..bcd67205b5b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -437,8 +437,7 @@ happens at run-time. At compile-time, a warning is raised if a destructuring spec doesn't use all optional fields. If the symbol `disallow-unknown-methods' is present, Eglot warns -on unknown notifications and errors on unknown requests. -")) +on unknown notifications and errors on unknown requests.")) (defun eglot--plist-keys (plist) (cl-loop for (k _v) on plist by #'cddr collect k)) From 0739cdcf20850aa94c7bbfd9791e344fe9202fd1 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sat, 15 Jan 2022 17:37:42 +0100 Subject: [PATCH 641/771] Improve backwards-compatibility of eglot-mode-map * eglot.el (eglot-mode-map): Only bind to eldoc-doc-buffer in Emacs 28.1 or later. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bcd67205b5b..72a0103c815 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1519,7 +1519,8 @@ 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] 'eldoc-doc-buffer) + (when (fboundp 'eldoc-doc-buffer) ; Emacs 28.1 or later + (define-key map [remap display-local-help] #'eldoc-doc-buffer)) map)) (defvar-local eglot--current-flymake-report-fn nil From abfb193201dd064c99a8c869434ffaacc4e0f66b Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sun, 16 Jan 2022 00:12:05 +0100 Subject: [PATCH 642/771] Remove unnecessary compatibility code * eglot.el (eglot-mode-map): Remove unnecessary compatibility code. We already depend on eldoc 0.11.0. --- lisp/progmodes/eglot.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 72a0103c815..0cc317fa7a2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1519,8 +1519,7 @@ and just return it. PROMPT shouldn't end with a question mark." ;;; (defvar eglot-mode-map (let ((map (make-sparse-keymap))) - (when (fboundp 'eldoc-doc-buffer) ; Emacs 28.1 or later - (define-key map [remap display-local-help] #'eldoc-doc-buffer)) + (define-key map [remap display-local-help] #'eldoc-doc-buffer) map)) (defvar-local eglot--current-flymake-report-fn nil From 97ded8227795da62d78d8b1952efd7d0351e492f Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sun, 16 Jan 2022 13:04:27 +0100 Subject: [PATCH 643/771] ; * eglot.el: move obsolete definition to new section. --- lisp/progmodes/eglot.el | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0cc317fa7a2..f3710fd3c98 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1565,9 +1565,6 @@ For example, to keep your Company customization use "Tell if current buffer is managed by EGLOT." eglot--managed-mode) -(make-obsolete-variable - 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") - (defvar eglot-managed-mode-hook nil "A hook run by EGLOT after it started/stopped managing a buffer. Use `eglot-managed-p' to determine if current buffer is managed.") @@ -3103,11 +3100,19 @@ If INTERACTIVE, prompt user for details." "Eclipse JDT breaks spec and replies with edits as arguments." (mapc #'eglot--apply-workspace-edit arguments)) + +;;; Obsolete +;;; + +(make-obsolete-variable 'eglot--managed-mode-hook + 'eglot-managed-mode-hook "1.6") + (provide 'eglot) -;;; eglot.el ends here ;; Local Variables: ;; bug-reference-bug-regexp: "\\(github#\\([0-9]+\\)\\)" ;; bug-reference-url-format: "https://github.com/joaotavora/eglot/issues/%s" ;; checkdoc-force-docstrings-flag: nil ;; End: + +;;; eglot.el ends here From 469835a4f20407d92549dd40a53652d107e26571 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sun, 16 Jan 2022 13:05:13 +0100 Subject: [PATCH 644/771] Obsolete eglot--plist-keys in favor of map-keys * eglot.el (eglot--plist-keys): Make into obsolete function alias for map-keys. --- lisp/progmodes/eglot.el | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f3710fd3c98..e7aa4ee1850 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -439,9 +439,6 @@ destructuring spec doesn't use all optional fields. If the symbol `disallow-unknown-methods' is present, Eglot warns on unknown notifications and errors on unknown requests.")) -(defun eglot--plist-keys (plist) - (cl-loop for (k _v) on plist by #'cddr collect k)) - (cl-defun eglot--check-object (interface-name object &optional @@ -454,11 +451,11 @@ on unknown notifications and errors on unknown requests.")) (eglot--interface interface-name) (when-let ((missing (and enforce-required (cl-set-difference required-keys - (eglot--plist-keys object))))) + (map-keys object))))) (eglot--error "A `%s' must have %s" interface-name missing)) (when-let ((excess (and disallow-non-standard (cl-set-difference - (eglot--plist-keys object) + (map-keys object) (append required-keys optional-keys))))) (eglot--error "A `%s' mustn't have %s" interface-name excess)) (when check-types @@ -583,7 +580,7 @@ treated as in `eglot-dbind'." ;; has all the keys the user wants to destructure. `(null (cl-set-difference ',vars-as-keywords - (eglot--plist-keys ,obj-once))))) + (map-keys ,obj-once))))) collect `(,condition (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,obj-once @@ -3106,6 +3103,7 @@ If INTERACTIVE, prompt user for details." (make-obsolete-variable 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") +(define-obsolete-function-alias 'eglot--plist-keys #'map-keys "1.9") (provide 'eglot) From 8b0ea132cb5e5a44c345fe97ebdab7d8a0d511fc Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sun, 16 Jan 2022 13:05:16 +0100 Subject: [PATCH 645/771] * eglot.el (eglot--server-capable): don't use obsolete name. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e7aa4ee1850..9c9f055cfa0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1466,7 +1466,7 @@ under cursor." (defun eglot--server-capable (&rest feats) "Determine if current server is capable of FEATS." (unless (cl-some (lambda (feat) - (memq feat eglot-ignored-server-capabilites)) + (memq feat eglot-ignored-server-capabilities)) feats) (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) then (cadr probe) From bc058058872a13e6ee3d9a04c6d37f2bba5a4852 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sun, 16 Jan 2022 14:13:05 +0100 Subject: [PATCH 646/771] ; unbreak tests on emacs 26 * eglot.el (eglot--plist-keys): Define in Emacs 26, no longer obsolete in Emacs 27 or later. (eglot--check-object): Go back to eglot--plist-keys. --- lisp/progmodes/eglot.el | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 9c9f055cfa0..14a4d1cd43a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -451,11 +451,11 @@ on unknown notifications and errors on unknown requests.")) (eglot--interface interface-name) (when-let ((missing (and enforce-required (cl-set-difference required-keys - (map-keys object))))) + (eglot--plist-keys object))))) (eglot--error "A `%s' must have %s" interface-name missing)) (when-let ((excess (and disallow-non-standard (cl-set-difference - (map-keys object) + (eglot--plist-keys object) (append required-keys optional-keys))))) (eglot--error "A `%s' mustn't have %s" interface-name excess)) (when check-types @@ -580,7 +580,7 @@ treated as in `eglot-dbind'." ;; has all the keys the user wants to destructure. `(null (cl-set-difference ',vars-as-keywords - (map-keys ,obj-once))))) + (eglot--plist-keys ,obj-once))))) collect `(,condition (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,obj-once @@ -3103,7 +3103,12 @@ If INTERACTIVE, prompt user for details." (make-obsolete-variable 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") -(define-obsolete-function-alias 'eglot--plist-keys #'map-keys "1.9") + +(if (< emacs-major-version 27) + (defun eglot--plist-keys (plist) + (cl-loop for (k _v) on plist by #'cddr collect k)) + ;; Make into an obsolete alias once we drop support for Emacs 26. + (defalias 'eglot--plist-keys #'map-keys)) (provide 'eglot) From 3aeebe8186dfcc7e4a6df638bd44b4740895427f Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Fri, 14 Jan 2022 13:03:44 +0100 Subject: [PATCH 647/771] Change rust language server to rust-analyzer rust-analyzer is the officially blessed Language Server for Rust: https://github.com/rust-lang/rfcs/pull/2912 Also drop the special support code for RLS. * eglot.el (eglot-server-programs): Add rust-mode language server "rust-analyzer" and prefer it to the older "rls". (eglot-rls, jsonrpc-connection-ready-p) (eglot-handle-notification): Delete special support for "rls". * eglot-tests.el (rls-analyzer-watches-files) (rls-analyzer-hover-after-edit): Rename to ... (rust-analyzer-watches-files) (rust-analyzer-hover-after-edit): ... this. Update tests to work with rust-analyzer. * README.md: Update references for RLS to point to rust-analyzer. * NEWS.md: Announce above change. GitHub-reference: per https://github.com/joaotavora/eglot/issues/803 --- lisp/progmodes/eglot.el | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 14a4d1cd43a..fb2f1ba100d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -142,7 +142,7 @@ chosen (interactively or automatically)." when probe return (cons probe args) finally (funcall err))))))) -(defvar eglot-server-programs `((rust-mode . (eglot-rls "rls")) +(defvar eglot-server-programs `((rust-mode . ("rust-analyzer" "rls")) (cmake-mode . ("cmake-language-server")) (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode @@ -2984,25 +2984,6 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) - -;;; Rust-specific -;;; -(defclass eglot-rls (eglot-lsp-server) () :documentation "Rustlang's RLS.") - -(cl-defmethod jsonrpc-connection-ready-p ((server eglot-rls) what) - "Except for :completion, RLS isn't ready until Indexing done." - (and (cl-call-next-method) - (or ;; RLS normally ready for this, even if building. - (eq :textDocument/completion what) - (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner server))) - (and (equal "Indexing" what) done))))) - -(cl-defmethod eglot-handle-notification - ((server eglot-rls) (_method (eql window/progress)) - &key id done title message &allow-other-keys) - "Handle notification window/progress." - (setf (eglot--spinner server) (list id title done message))) - ;;; eclipse-jdt-specific ;;; From 27ba1994fe50ebdcd927bb46fccc9db0d67e9408 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Mon, 17 Jan 2022 23:32:50 +0100 Subject: [PATCH 648/771] ; fix thinko in last commit --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fb2f1ba100d..219bcc85bd9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -142,7 +142,7 @@ chosen (interactively or automatically)." when probe return (cons probe args) finally (funcall err))))))) -(defvar eglot-server-programs `((rust-mode . ("rust-analyzer" "rls")) +(defvar eglot-server-programs `((rust-mode . ,(eglot-alternatives '("rust-analyzer" "rls"))) (cmake-mode . ("cmake-language-server")) (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode From 8a9959e054b1faf8305354e1ff7e6e1e92a216a2 Mon Sep 17 00:00:00 2001 From: Derek Passen Date: Tue, 18 Jan 2022 19:56:13 -0600 Subject: [PATCH 649/771] Add clojure-lsp support for clojure * eglot.el (eglot-server-programs): Add clojure-lsp for Clojure. * README.md: Document the above change. Copyright-paperwork-exempt: Yes --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 219bcc85bd9..10e16162746 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -187,7 +187,8 @@ language-server/bin/php-language-server.php")) (css-mode . ,(eglot-alternatives '(("vscode-css-language-server" "--stdio") ("css-languageserver" "--stdio")))) (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) - (dockerfile-mode . ("docker-langserver" "--stdio"))) + (dockerfile-mode . ("docker-langserver" "--stdio")) + (clojure-mode . ("clojure-lsp"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific From 5b62d0071ec55ab8a38a0ee8f7fbd491f5731e22 Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Thu, 20 Jan 2022 23:06:53 +0100 Subject: [PATCH 650/771] Enable lsp project-wide diagnostics via flymake * eglot.el (eglot-handle-notification): Pass on diagnostics from unvisited files to flymake. Enables project-wide-diagnostics, so that we can view all diagnostics in a given workspace. Uses new functionality from flymake 1.2.1, hence the version bump. * eglot-tests.el (project-wide-diagnostics-typescript): New tests showcasing the possibility to see all related diagnostics in a workspace. * eglot-tests.el (project-wide-diagnostics-rust-analyzer): New tests showcasing the possibility to see all related diagnostics in a workspace. * NEWS.md: Mention the new functionality * README.md: Mention the new functionality --- lisp/progmodes/eglot.el | 105 +++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 10e16162746..e27ddd7f940 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.14") (flymake "1.0.9") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0")) ;; This file is part of GNU Emacs. @@ -1825,49 +1825,66 @@ COMMAND is a symbol naming the command." (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' "Handle notification publishDiagnostics." - (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) - (with-current-buffer buffer - (cl-loop - for diag-spec across diagnostics - collect (eglot--dbind ((Diagnostic) range message severity source tags) - diag-spec - (setq message (concat source ": " message)) - (pcase-let - ((sev severity) - (`(,beg . ,end) (eglot--range-region range))) - ;; Fallback to `flymake-diag-region' if server - ;; botched the range - (when (= beg end) - (if-let* ((st (plist-get range :start)) - (diag-region - (flymake-diag-region - (current-buffer) (1+ (plist-get st :line)) - (plist-get st :character)))) - (setq beg (car diag-region) end (cdr diag-region)) - (eglot--widening - (goto-char (point-min)) - (setq beg - (point-at-bol - (1+ (plist-get (plist-get range :start) :line)))) - (setq end - (point-at-eol - (1+ (plist-get (plist-get range :end) :line))))))) - (eglot--make-diag (current-buffer) beg end - (cond ((null sev) 'eglot-error) - ((<= sev 1) 'eglot-error) - ((= sev 2) 'eglot-warning) - (t 'eglot-note)) - message `((eglot-lsp-diag . ,diag-spec)) - (and tags - `((face . ,(mapcar (lambda (tag) - (alist-get tag eglot--tag-faces)) - tags))))))) - into diags - finally (cond (eglot--current-flymake-report-fn - (eglot--report-to-flymake diags)) - (t - (setq eglot--unreported-diagnostics (cons t diags)))))) - (jsonrpc--debug server "Diagnostics received for unvisited %s" uri))) + (cl-flet ((eglot--diag-type (sev) + (cond ((null sev) 'eglot-error) + ((<= sev 1) 'eglot-error) + ((= sev 2) 'eglot-warning) + (t 'eglot-note)))) + (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) + (with-current-buffer buffer + (cl-loop + for diag-spec across diagnostics + collect (eglot--dbind ((Diagnostic) range message severity source tags) + diag-spec + (setq message (concat source ": " message)) + (pcase-let + ((`(,beg . ,end) (eglot--range-region range))) + ;; Fallback to `flymake-diag-region' if server + ;; botched the range + (when (= beg end) + (if-let* ((st (plist-get range :start)) + (diag-region + (flymake-diag-region + (current-buffer) (1+ (plist-get st :line)) + (plist-get st :character)))) + (setq beg (car diag-region) end (cdr diag-region)) + (eglot--widening + (goto-char (point-min)) + (setq beg + (point-at-bol + (1+ (plist-get (plist-get range :start) :line)))) + (setq end + (point-at-eol + (1+ (plist-get (plist-get range :end) :line))))))) + (eglot--make-diag + (current-buffer) beg end + (eglot--diag-type severity) + message `((eglot-lsp-diag . ,diag-spec)) + (and tags + `((face + . ,(mapcar (lambda (tag) + (alist-get tag eglot--tag-faces)) + tags))))))) + into diags + finally (cond (eglot--current-flymake-report-fn + (eglot--report-to-flymake diags)) + (t + (setq eglot--unreported-diagnostics (cons t diags)))))) + (cl-loop + with path = (expand-file-name (eglot--uri-to-path uri)) + for diag-spec across diagnostics + collect (eglot--dbind ((Diagnostic) range message severity source) diag-spec + (setq message (concat source ": " message)) + (let* ((start (plist-get range :start)) + (line (1+ (plist-get start :line))) + (char (1+ (plist-get start :character)))) + (eglot--make-diag + path (cons line char) nil (eglot--diag-type severity) message))) + into diags + finally + (setq flymake-list-only-diagnostics + (assoc-delete-all path flymake-list-only-diagnostics #'string=)) + (push (cons path diags) flymake-list-only-diagnostics))))) (cl-defun eglot--register-unregister (server things how) "Helper for `registerCapability'. From 6a6f4c3d27892ee5355d341ae9f586a48d217023 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sat, 22 Jan 2022 04:13:11 +0100 Subject: [PATCH 651/771] Don't use :exclusive no See https://github.com/joaotavora/eglot/issues/812 for background, in particular: https://github.com/joaotavora/eglot/issues/812#issuecomment-1014821345 * eglot.el (eglot-completion-at-point): Don't use :exclusive no, as it leads to breakage in many cases. GitHub-reference: close https://github.com/joaotavora/eglot/issues/812 --- lisp/progmodes/eglot.el | 1 - 1 file changed, 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e27ddd7f940..d8308e0f978 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2489,7 +2489,6 @@ is not active." (regexp-opt (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) (line-beginning-position)))) - :exclusive 'no :exit-function (lambda (proxy status) (when (eq status 'finished) From 97107540806a426c0951e572890b2827d41ebd43 Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Wed, 12 Jan 2022 19:06:18 -0800 Subject: [PATCH 652/771] Add support for optional completionitem.tags * eglot.el (eglot--lsp-interface-alist): Add optional CompletionItem.tags. (eglot-completion-at-point): Add :company-deprecated key and value, checking for either the appropriate tag (1) in the :tags property, or a truthy value for the :deprecated property. (eglot-client-capabilities): Advertise tagSupport (for tag == 1) and deprecatedSupport. Also load an updated version of seq.el in Emacsen < 27. * Makefile (ELPA_DEPS): Require latest seq.el. --- lisp/progmodes/eglot.el | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d8308e0f978..f109fc930ff 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/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.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0")) +;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0") (seq "2.23")) ;; This file is part of GNU Emacs. @@ -62,7 +62,6 @@ (require 'imenu) (require 'cl-lib) (require 'project) -(require 'seq) (require 'url-parse) (require 'url-util) (require 'pcase) @@ -85,6 +84,11 @@ (load "eldoc") (require 'eldoc)) +;; Similar issue as above for Emacs 26.3 and seq.el. +(if (< emacs-major-version 27) + (load "seq") + (require 'seq)) + ;; forward-declare, but don't require (Emacs 28 doesn't seem to care) (defvar markdown-fontify-code-blocks-natively) (defvar company-backends) @@ -365,7 +369,7 @@ This can be useful when using docker to run a language server.") (:kind :detail :documentation :deprecated :preselect :sortText :filterText :insertText :insertTextFormat :textEdit :additionalTextEdits :commitCharacters - :command :data)) + :command :data :tags)) (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription :tags)) (DocumentHighlight (:range) (:kind)) (FileSystemWatcher (:globPattern) (:kind)) @@ -659,7 +663,9 @@ treated as in `eglot-dbind'." `(:snippetSupport ,(if (eglot--snippet-expansion-fn) t - :json-false)) + :json-false) + :deprecatedSupport t + :tagSupport (:valueSet [1])) :contextSupport t) :hover (list :dynamicRegistration :json-false :contentFormat @@ -2461,6 +2467,12 @@ is not active." (kind (alist-get (plist-get lsp-item :kind) eglot--kind-names))) (intern (downcase kind)))) + :company-deprecated + (lambda (proxy) + (when-let ((lsp-item (get-text-property 0 'eglot--lsp-item proxy))) + (or (seq-contains-p (plist-get lsp-item :tags) + 1) + (plist-get lsp-item :deprecated)))) :company-docsig ;; FIXME: autoImportText is specific to the pyright language server (lambda (proxy) From ed4fd33223e6cc2bafd7323abc91e0bee23accb4 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Sat, 22 Jan 2022 04:40:12 +0100 Subject: [PATCH 653/771] * eglot.el (eglot-handle-notification): silence byte-compiler. --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f109fc930ff..957ddde68c2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1828,8 +1828,8 @@ COMMAND is a symbol naming the command." "Handle notification telemetry/event.") ;; noop, use events buffer (cl-defmethod eglot-handle-notification - (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics - &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' + (_server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics + &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' "Handle notification publishDiagnostics." (cl-flet ((eglot--diag-type (sev) (cond ((null sev) 'eglot-error) From 8b31247f1c6626b289e75460e0d0978bf446e05a Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Sat, 22 Jan 2022 19:59:06 -0800 Subject: [PATCH 654/771] Properly check the completionitem.deprecated property * eglot.el (eglot-completion-at-point): Check the :deprecated property is `t'. We do this so that a :deprecated property of :json-false does not cause a completion candidate to be incorrectly marked as deprecated. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 957ddde68c2..5a0a8caba48 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2472,7 +2472,7 @@ is not active." (when-let ((lsp-item (get-text-property 0 'eglot--lsp-item proxy))) (or (seq-contains-p (plist-get lsp-item :tags) 1) - (plist-get lsp-item :deprecated)))) + (eq t (plist-get lsp-item :deprecated))))) :company-docsig ;; FIXME: autoImportText is specific to the pyright language server (lambda (proxy) From 4f1f06375a219178ba681a6101af1fece73024b0 Mon Sep 17 00:00:00 2001 From: Brian Leung Date: Tue, 1 Mar 2022 07:59:05 -0800 Subject: [PATCH 655/771] Prevent empty diagnostic tags vector hiding main fontification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot-handle-notification): Require that the resulting list of faces is non-empty and that each face corresponds only to a known tag. For unknown tags, we don't pass any additional face information to Flymake, and instead expect it to make the appropriate overlay with the "severity" property of the Diagnostic. Co-authored-by: João Távora GitHub-reference: fix https://github.com/joaotavora/eglot/issues/851 --- lisp/progmodes/eglot.el | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5a0a8caba48..3f84b3b7a26 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1866,11 +1866,11 @@ COMMAND is a symbol naming the command." (current-buffer) beg end (eglot--diag-type severity) message `((eglot-lsp-diag . ,diag-spec)) - (and tags - `((face - . ,(mapcar (lambda (tag) - (alist-get tag eglot--tag-faces)) - tags))))))) + (when-let ((faces + (cl-loop for tag across tags + when (alist-get tag eglot--tag-faces) + collect it))) + `((face . ,faces)))))) into diags finally (cond (eglot--current-flymake-report-fn (eglot--report-to-flymake diags)) From 85ecf46a18c05d4182af3e3a11a8fadd421a28d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 6 Mar 2022 11:15:18 +0000 Subject: [PATCH 656/771] Have a couple of lsp faces inherit from basic "shadow" * eglot.el (eglot-diagnostic-tag-unnecessary-face) (eglot-diagnostic-tag-deprecated-face): Inherit from 'shadow'. GitHub-reference: per https://github.com/joaotavora/eglot/issues/858 --- lisp/progmodes/eglot.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3f84b3b7a26..7bff005973a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -258,11 +258,11 @@ CONTACT can be: "Face for package-name in EGLOT's mode line.") (defface eglot-diagnostic-tag-unnecessary-face - '((t . (:weight ultra-light))) + '((t (:inherit shadow))) "Face used to render unused or unnecessary code.") (defface eglot-diagnostic-tag-deprecated-face - '((t . (:strike-through t))) + '((t . (:inherit shadow :strike-through t))) "Face used to render deprecated or obsolete code.") (defcustom eglot-autoreconnect 3 From cb562118cb14d8bb71140a07232107d83bbbf6d9 Mon Sep 17 00:00:00 2001 From: Augusto Stoffel Date: Thu, 10 Mar 2022 12:32:20 +0100 Subject: [PATCH 657/771] Don't strip invisible text when formatting hover string This was introduced in https://github.com/joaotavora/eglot/issues/482 due to a bad interaction with a specific server. But this solution makes hyperlinks in Eldoc buffers unclickable, because the markdown-mode function that visits a link relies on the invisible text. Per https://github.com/joaotavora/eglot/issues/866 * eglot.el (eglot--format-markup): Use buffer-string instead of filter-buffer-substring GitHub-reference: fix https://github.com/joaotavora/eglot/issues/865 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7bff005973a..b84e1449d74 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1436,7 +1436,7 @@ Doubles as an indicator of snippet support." (message-log-max nil)) (ignore-errors (delay-mode-hooks (funcall mode)))) (font-lock-ensure) - (string-trim (filter-buffer-substring (point-min) (point-max)))))) + (string-trim (buffer-string))))) (define-obsolete-variable-alias 'eglot-ignored-server-capabilites 'eglot-ignored-server-capabilities "1.8") From 77f3157dcd2926ded5a04069ef44242b3c27a2a5 Mon Sep 17 00:00:00 2001 From: Manuel Uberti Date: Fri, 11 Mar 2022 13:41:53 +0100 Subject: [PATCH 658/771] Use new jdtls script for eclipse jdt Per https://github.com/joaotavora/eglot/issues/864. * eglot.el (eglot-server-programs): use new jdtls (eglot--eclipse-jdt-contact, eglot--eclipse-jdt). Remove. (eglot-execute-command eglot-eclipse-jdt): Remove. (eglot-initialization-options eglot-eclipse-jdt): Remove. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/863 --- lisp/progmodes/eglot.el | 96 +---------------------------------------- 1 file changed, 1 insertion(+), 95 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b84e1449d74..5b4d4196780 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -173,7 +173,7 @@ language-server/bin/php-language-server.php")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) - (java-mode . eglot--eclipse-jdt-contact) + (java-mode . ("jdtls")) (dart-mode . ("dart_language_server")) (elixir-mode . ("language_server.sh")) (ada-mode . ("ada_language_server")) @@ -3013,100 +3013,6 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) - -;;; eclipse-jdt-specific -;;; -(defclass eglot-eclipse-jdt (eglot-lsp-server) () - :documentation "Eclipse's Java Development Tools Language Server.") - -(cl-defmethod eglot-initialization-options ((server eglot-eclipse-jdt)) - "Passes through required jdt initialization options." - `(:workspaceFolders - [,@(cl-delete-duplicates - (mapcar #'eglot--path-to-uri - (let* ((root (project-root (eglot--project server)))) - (cons root - (mapcar - #'file-name-directory - (append - (file-expand-wildcards (concat root "*/pom.xml")) - (file-expand-wildcards (concat root "*/build.gradle")) - (file-expand-wildcards (concat root "*/.project"))))))) - :test #'string=)] - ,@(if-let ((home (or (getenv "JAVA_HOME") - (ignore-errors - (expand-file-name - ".." - (file-name-directory - (file-chase-links (executable-find "javac")))))))) - `(:settings (:java (:home ,home))) - (ignore (eglot--warn "JAVA_HOME env var not set"))))) - -(defun eglot--eclipse-jdt-contact (interactive) - "Return a contact for connecting to eclipse.jdt.ls server, as a cons cell. -If INTERACTIVE, prompt user for details." - (cl-labels - ((is-the-jar - (path) - (and (string-match-p - "org\\.eclipse\\.equinox\\.launcher_.*\\.jar$" - (file-name-nondirectory path)) - (file-exists-p path)))) - (let* ((classpath (or (getenv "CLASSPATH") path-separator)) - (cp-jar (cl-find-if #'is-the-jar (split-string classpath path-separator))) - (jar cp-jar) - (dir - (cond - (jar (file-name-as-directory - (expand-file-name ".." (file-name-directory jar)))) - (interactive - (expand-file-name - (read-directory-name - (concat "Path to eclipse.jdt.ls directory (could not" - " find it in CLASSPATH): ") - nil nil t))) - (t (error "Could not find eclipse.jdt.ls jar in CLASSPATH")))) - (repodir - (concat dir - "org.eclipse.jdt.ls.product/target/repository/")) - (repodir (if (file-directory-p repodir) repodir dir)) - (config - (concat - repodir - (cond - ((string= system-type "darwin") "config_mac") - ((string= system-type "windows-nt") "config_win") - (t "config_linux")))) - (workspace - (expand-file-name (md5 (project-root (eglot--current-project))) - (locate-user-emacs-file - "eglot-eclipse-jdt-cache")))) - (unless jar - (setq jar - (cl-find-if #'is-the-jar - (directory-files (concat repodir "plugins") t)))) - (unless (and jar (file-exists-p jar) (file-directory-p config)) - (error "Could not find required eclipse.jdt.ls files (build required?)")) - (when (and interactive (not cp-jar) - (y-or-n-p (concat "Add path to the server program " - "to CLASSPATH environment variable?"))) - (setenv "CLASSPATH" (concat (getenv "CLASSPATH") path-separator jar))) - (unless (file-directory-p workspace) - (make-directory workspace t)) - (cons 'eglot-eclipse-jdt - (list (executable-find "java") - "-Declipse.application=org.eclipse.jdt.ls.core.id1" - "-Dosgi.bundles.defaultStartLevel=4" - "-Declipse.product=org.eclipse.jdt.ls.core.product" - "-jar" jar - "-configuration" config - "-data" workspace))))) - -(cl-defmethod eglot-execute-command - ((_server eglot-eclipse-jdt) (_cmd (eql java.apply.workspaceEdit)) arguments) - "Eclipse JDT breaks spec and replies with edits as arguments." - (mapc #'eglot--apply-workspace-edit arguments)) - ;;; Obsolete ;;; From be4755233192f2e27ddab75a75c6b858198bac26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Sat, 12 Mar 2022 10:57:53 +0100 Subject: [PATCH 659/771] Change capability 'documentchanges' to t Eglot does support woskspaceEdit/documentChanges, but failed to advertise this fact. Per https://github.com/joaotavora/eglot/issues/873. * eglot.el (eglot-client-capabilities): Set documentChanges to t. GitHub-reference: per https://github.com/joaotavora/eglot/issues/853 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 5b4d4196780..cc0a06f4cbd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -649,7 +649,7 @@ treated as in `eglot-dbind'." :workspace (list :applyEdit t :executeCommand `(:dynamicRegistration :json-false) - :workspaceEdit `(:documentChanges :json-false) + :workspaceEdit `(:documentChanges t) :didChangeWatchedFiles `(:dynamicRegistration t) :symbol `(:dynamicRegistration :json-false) :configuration t) From 349f6b5f7894667b5acb5a6a3dd93bcf27751c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 15 Mar 2022 10:20:24 +0000 Subject: [PATCH 660/771] Don't advertise didchangewatchedfiles on tramp * eglot.el (eglot--trampish-p): New helper. (eglot-client-capabilities): Use it. (eglot--uri-to-path): Use it. GitHub-reference: per https://github.com/joaotavora/eglot/issues/883 --- lisp/progmodes/eglot.el | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cc0a06f4cbd..ea9299ab59e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -644,13 +644,15 @@ treated as in `eglot-dbind'." (cl-defgeneric eglot-client-capabilities (server) "What the EGLOT LSP client supports for SERVER." - (:method (_s) + (:method (s) (list :workspace (list :applyEdit t :executeCommand `(:dynamicRegistration :json-false) :workspaceEdit `(:documentChanges t) - :didChangeWatchedFiles `(:dynamicRegistration t) + :didChangeWatchedFiles + `(:dynamicRegistration + ,(if (eglot--trampish-p s) :json-false t)) :symbol `(:dynamicRegistration :json-false) :configuration t) :textDocument @@ -1401,9 +1403,7 @@ If optional MARKER, return a marker instead" "Convert URI to file path, helped by `eglot--current-server'." (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) (let* ((server (eglot-current-server)) - (remote-prefix (and server - (file-remote-p - (project-root (eglot--project server))))) + (remote-prefix (and server (eglot--trampish-p server))) (retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) ;; Remove the leading "/" for local MS Windows-style paths. (normalized (if (and (not remote-prefix) @@ -1518,6 +1518,10 @@ and just return it. PROMPT shouldn't end with a question mark." (cl-find read servers :key name :test #'equal))) (t (car servers))))) +(defun eglot--trampish-p (server) + "Tell if SERVER's project root is `file-remote-p'." + (file-remote-p (project-root (eglot--project server)))) + ;;; Minor modes ;;; From 6dba74d8cb7e0172641ac6fdd7dfcd80bd22a1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 18 Mar 2022 10:54:21 +0000 Subject: [PATCH 661/771] Defend against broken move-to-column in recent emacs * eglot.el (eglot-lsp-abiding-column): Use (min (point) (point-max)) This is a defensive fix for an Emacs/company-mode problem described below. The problem can be reproduced in Eglot before this commit with: ~/Source/Emacs/emacs/src/emacs -Q -f package-initialize -L \ ~/Source/Emacs/company-mode -l company -f global-company-mode -l \ eglot.el ~/tmp/issue-860/args_out_of_range.c -f eglot -f \ display-line-numbers-mode -f toggle-debug-on-error 1 // args_out_of_range.c 2 struct Book { 3 int id; 4 char title[50] 5 } book = { 1024, "C" }; 6 7 int main(int argc, char *argv[]) 8 { 9 10 // Error when typing the dot to make "book." 11 book 12 return 0; 13 } When one types the dot after the "book" on line 11, company-mode displays a two-line overlay that visually encompasses line 12 after "book", which has the "return 0;" statement. That line happens to also hold a warning about incorrect syntax, one that starts at column 2. Eglot uses 'move-to-column' to go that precise place. In Emacs 27.2, move-to-column is unaffected by previous company-mode overlays, even if the current line is being co-used visually by the overlay. It moves to the right buffer position. In Emacs master, this isn't true. It seems to be confounded by the company-mode overlay and moves to eob, which eventually breaks Eglot with a backtrace such as this one: Debugger entered--Lisp error: (args-out-of-range # 110 124) encode-coding-region(110 124 utf-16 t) (length (encode-coding-region (or lbp (line-beginning-position)) (point) 'utf-16 t)) (- (length (encode-coding-region (or lbp (line-beginning-position)) (point) 'utf-16 t)) 2) (/ (- (length (encode-coding-region (or lbp (line-beginning-position)) (point) 'utf-16 t)) 2) 2) eglot-lsp-abiding-column(110) (- column (eglot-lsp-abiding-column lbp)) (setq diff (- column (eglot-lsp-abiding-column lbp))) (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (while (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (condition-case eob-err (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) (end-of-buffer (throw '--cl-block-nil-- eob-err))) (setq --cl-var-- nil)) (let* ((lbp (line-beginning-position)) (diff nil) (--cl-var-- t)) (narrow-to-region lbp (line-end-position)) (move-to-column column) (while (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (condition-case eob-err (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) (end-of-buffer (throw '--cl-block-nil-- eob-err))) (setq --cl-var-- nil)) nil) (catch '--cl-block-nil-- (let* ((lbp (line-beginning-position)) (diff nil) (--cl-var-- t)) (narrow-to-region lbp (line-end-position)) (move-to-column column) (while (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (condition-case eob-err (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) (end-of-buffer (throw '--cl-block-nil-- eob-err))) (setq --cl-var-- nil)) nil)) (save-restriction (catch '--cl-block-nil-- (let* ((lbp (line-beginning-position)) (diff nil) (--cl-var-- t)) (narrow-to-region lbp (line-end-position)) (move-to-column column) (while (progn (setq diff (- column (eglot-lsp-abiding-column lbp))) (not (= 0 diff))) (condition-case eob-err (forward-char (/ (if ... ... ...) 2)) (end-of-buffer (throw '--cl-block-nil-- eob-err))) (setq --cl-var-- nil)) nil))) eglot-move-to-lsp-abiding-column(2) GitHub-reference: fix https://github.com/joaotavora/eglot/issues/860 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ea9299ab59e..3bd2d844c8a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1312,7 +1312,8 @@ fully LSP-compliant servers, this should be set to "Calculate current COLUMN as defined by the LSP spec. LBP defaults to `line-beginning-position'." (/ (- (length (encode-coding-region (or lbp (line-beginning-position)) - (point) 'utf-16 t)) + ;; Fix github#860 + (min (point) (point-max)) 'utf-16 t)) 2) 2)) From a38ce8b28fef2f50263d827aedb3394b795dac56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Sun, 20 Mar 2022 09:50:15 +0100 Subject: [PATCH 662/771] Add simple support for workspacefolders Close https://github.com/joaotavora/eglot/issues/893. Clients can support workspaceFolders since LSP 3.6. rootUri and rootPath are deprecated. Dynamic changes in folders are not supported, i.e., this patch does not implement workspace/didChangeWorkspaceFolders. * eglot.el (eglot-client-capabilities): Add capability `workspaceFolders'. (eglot-workspace-folders): New cl-defgeneric. (eglot--connect): Add workspaceFolders to initializeParams. (eglot-handle-request workspace/workspaceFolders): New cl-defmethod. --- lisp/progmodes/eglot.el | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3bd2d844c8a..daf6c3e2376 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -654,7 +654,8 @@ treated as in `eglot-dbind'." `(:dynamicRegistration ,(if (eglot--trampish-p s) :json-false t)) :symbol `(:dynamicRegistration :json-false) - :configuration t) + :configuration t + :workspaceFolders t) :textDocument (list :synchronization (list @@ -719,6 +720,15 @@ treated as in `eglot-dbind'." #'car eglot--tag-faces)]))) :experimental eglot--{}))) +(cl-defgeneric eglot-workspace-folders (server) + "Return workspaceFolders for SERVER." + (let ((project (eglot--project server))) + (vconcat + (mapcar (lambda (dir) + (list :uri (eglot--path-to-uri dir) + :name (abbreviate-file-name dir))) + `(,(project-root project) ,@(project-external-roots project)))))) + (defclass eglot-lsp-server (jsonrpc-process-connection) ((project-nickname :documentation "Short nickname for the associated project." @@ -1164,7 +1174,8 @@ This docstring appeases checkdoc, that's all." :rootUri (eglot--path-to-uri default-directory) :initializationOptions (eglot-initialization-options server) - :capabilities (eglot-client-capabilities server)) + :capabilities (eglot-client-capabilities server) + :workspaceFolders (eglot-workspace-folders server)) :success-fn (eglot--lambda ((InitializeResult) capabilities serverInfo) (unless cancelled @@ -1924,6 +1935,11 @@ THINGS are either registrations or unregisterations (sic)." "Handle server request workspace/applyEdit." (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) +(cl-defmethod eglot-handle-request + (server (_method (eql workspace/workspaceFolders))) + "Handle server request workspace/workspaceFolders." + (eglot-workspace-folders server)) + (defun eglot--TextDocumentIdentifier () "Compute TextDocumentIdentifier object for current buffer." `(:uri ,(eglot--path-to-uri (or buffer-file-name From 965e1378f19d36284119324a06840752dfff3990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 24 Mar 2022 16:06:08 +0000 Subject: [PATCH 663/771] Use bounds of thing at point when asking for code actions * eglot.el (eglot--region-bounds): Consider bounds of things at point. GitHub-reference: per https://github.com/joaotavora/eglot/issues/895 --- lisp/progmodes/eglot.el | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index daf6c3e2376..83a29455147 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2838,8 +2838,11 @@ is not active." :newName ,newname)) current-prefix-arg)) -(defun eglot--region-bounds () "Region bounds if active, else point and nil." - (if (use-region-p) `(,(region-beginning) ,(region-end)) `(,(point) nil))) +(defun eglot--region-bounds () + "Region bounds if active, else bounds of things at point." + (if (use-region-p) `(,(region-beginning) ,(region-end)) + (let ((boftap (bounds-of-thing-at-point 'sexp))) + (list (car boftap) (cdr boftap))))) (defun eglot-code-actions (beg &optional end action-kind) "Offer to execute actions of ACTION-KIND between BEG and END. From 05418a1d836cc76d3e4c8bf5287b8351da523096 Mon Sep 17 00:00:00 2001 From: Marcus Swanson Date: Sat, 26 Mar 2022 22:43:31 +0100 Subject: [PATCH 664/771] Add omnisharp support for c# * eglot.el (eglot-server-programs): Add omnisharp for C#. * README.md: Document the above change. Copyright-paperwork-exempt: Yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/897 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 83a29455147..1c162408905 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -192,7 +192,8 @@ language-server/bin/php-language-server.php")) (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) (dockerfile-mode . ("docker-langserver" "--stdio")) - (clojure-mode . ("clojure-lsp"))) + (clojure-mode . ("clojure-lsp")) + (csharp-mode . ("omnisharp" "-lsp"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific From 5d2f6bc667afb8252170690ad9523f327d93bc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Sun, 27 Mar 2022 22:58:44 +0200 Subject: [PATCH 665/771] Map more emacs variables to lsp formattingoptions fields * eglot.el (eglot-format): Map require-final-newline to insertFinalNewline and delete-trailing-lines to trimFinalNewlines. GitHub-reference: close https://github.com/joaotavora/eglot/issues/900 --- lisp/progmodes/eglot.el | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1c162408905..58ad4588aed 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2381,7 +2381,9 @@ is not active." (cl-list* :textDocument (eglot--TextDocumentIdentifier) :options (list :tabSize tab-width - :insertSpaces (if indent-tabs-mode :json-false t)) + :insertSpaces (if indent-tabs-mode :json-false t) + :insertFinalNewline (if require-final-newline t :json-false) + :trimFinalNewlines (if delete-trailing-lines t :json-false)) args) :deferred method)))) From c2d97d22aa535af2640676e1df05a41f9abbd1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Wed, 23 Mar 2022 21:47:45 +0100 Subject: [PATCH 666/771] Implement on-type-formatting support * eglot.el (eglot-format): Add new optional argument `on-type-format' to request :textDocument/onTypeFormatting, and ... (eglot--post-self-insert-hook): ... call it from here when necessary. * eglot-tests.el (eglot--simulate-key-event): New helper defun. (rust-on-type-formatting): New test. * NEWS.md: mention feature. GitHub-reference: close https://github.com/joaotavora/eglot/issues/899 --- lisp/progmodes/eglot.el | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58ad4588aed..1cf0b7ae639 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1974,8 +1974,18 @@ THINGS are either registrations or unregisterations (sic)." "If non-nil, value of the last inserted character in buffer.") (defun eglot--post-self-insert-hook () - "Set `eglot--last-inserted-char'." - (setq eglot--last-inserted-char last-input-event)) + "Set `eglot--last-inserted-char', call on-type-formatting if necessary." + (setq eglot--last-inserted-char last-input-event) + (when (or (eq last-input-event + (elt (eglot--server-capable + :documentOnTypeFormattingProvider + :firstTriggerCharacter) + 0)) + (seq-find (lambda (elt) (eq last-input-event (elt elt 0))) + (eglot--server-capable + :documentOnTypeFormattingProvider + :moreTriggerCharacter))) + (eglot-format (point) nil (string last-input-event)))) (defun eglot--pre-command-hook () "Reset `eglot--last-inserted-char'." @@ -2357,14 +2367,23 @@ Try to visit the target file for a richer summary line." (interactive) (eglot-format nil nil)) -(defun eglot-format (&optional beg end) +(defun eglot-format (&optional beg end on-type-format) "Format region BEG END. If either BEG or END is nil, format entire buffer. Interactively, format active region, or entire buffer if region -is not active." +is not active. + +If ON-TYPE-FORMAT is non-nil, request on-type-formatting from the +server. The argument should be a one-character-long string that +has just been inserted at BEG." (interactive (and (region-active-p) (list (region-beginning) (region-end)))) (pcase-let ((`(,method ,cap ,args) (cond + ((and beg on-type-format) + `(:textDocument/onTypeFormatting + :documentOnTypeFormattingProvider + ,`(:position ,(eglot--pos-to-lsp-position beg) + :ch ,on-type-format))) ((and beg end) `(:textDocument/rangeFormatting :documentRangeFormattingProvider From 6d815aaa986f24d356aa08557095cbaab3414142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 29 Mar 2022 00:09:34 +0100 Subject: [PATCH 667/771] Tweak on-type-formatting code * eglot.el (eglot--post-self-insert-hook): Tweak. (eglot-format): Tweak docstring. GitHub-reference: per https://github.com/joaotavora/eglot/issues/899 --- lisp/progmodes/eglot.el | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1cf0b7ae639..d71e5966a83 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1974,18 +1974,16 @@ THINGS are either registrations or unregisterations (sic)." "If non-nil, value of the last inserted character in buffer.") (defun eglot--post-self-insert-hook () - "Set `eglot--last-inserted-char', call on-type-formatting if necessary." + "Set `eglot--last-inserted-char', maybe call on-type-formatting." (setq eglot--last-inserted-char last-input-event) - (when (or (eq last-input-event - (elt (eglot--server-capable - :documentOnTypeFormattingProvider - :firstTriggerCharacter) - 0)) - (seq-find (lambda (elt) (eq last-input-event (elt elt 0))) - (eglot--server-capable - :documentOnTypeFormattingProvider - :moreTriggerCharacter))) - (eglot-format (point) nil (string last-input-event)))) + (let ((ot-provider (eglot--server-capable :documentOnTypeFormattingProvider))) + (when (and ot-provider + (or (eq last-input-event + (elt (plist-get ot-provider :firstTriggerCharacter) 0)) + (cl-find last-input-event + (plist-get ot-provider :moreTriggerCharacter) + :key #'seq-first))) + (eglot-format (point) nil last-input-event)))) (defun eglot--pre-command-hook () "Reset `eglot--last-inserted-char'." @@ -2373,9 +2371,8 @@ If either BEG or END is nil, format entire buffer. Interactively, format active region, or entire buffer if region is not active. -If ON-TYPE-FORMAT is non-nil, request on-type-formatting from the -server. The argument should be a one-character-long string that -has just been inserted at BEG." +If non-nil, ON-TYPE-FORMAT is a character just inserted at BEG +for which LSP on-type-formatting should be requested." (interactive (and (region-active-p) (list (region-beginning) (region-end)))) (pcase-let ((`(,method ,cap ,args) (cond @@ -2383,7 +2380,7 @@ has just been inserted at BEG." `(:textDocument/onTypeFormatting :documentOnTypeFormattingProvider ,`(:position ,(eglot--pos-to-lsp-position beg) - :ch ,on-type-format))) + :ch ,(string on-type-format)))) ((and beg end) `(:textDocument/rangeFormatting :documentRangeFormattingProvider From 904556f662fa188c97f3b56d3555c58a3dc84afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 28 Mar 2022 11:00:44 +0100 Subject: [PATCH 668/771] Easier initializationoptions in eglot-server-programs Per https://github.com/joaotavora/eglot/issues/845. * NEWS.md: Update. * eglot.el (eglot-server-programs): Document new syntax. (eglot-initialization-options): Can use initializationOptions from server's saved initargs. (eglot--connect): Allow a plist to be appended to a server contact. GitHub-reference: close https://github.com/joaotavora/eglot/issues/901 --- lisp/progmodes/eglot.el | 51 ++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d71e5966a83..ade8a7c7118 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -218,17 +218,24 @@ CONTACT can be: PROGRAM is called with ARGS and is expected to serve LSP requests over the standard input/output channels. +* A list (PROGRAM [ARGS...] :initializationOptions OPTIONS), + whereupon PROGRAM is called with ARGS as in the first option, + and the LSP \"initializationOptions\" JSON object is + constructed from OPTIONS. If OPTIONS is a unary function, it + is called with the server instance and should return a JSON + object. + * A list (HOST PORT [TCP-ARGS...]) where HOST is a string and PORT is a positive integer for connecting to a server via TCP. Remaining ARGS are passed to `open-network-stream' for upgrading the connection with encryption or other capabilities. * A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereupon a - combination of the two previous options is used. First, an - attempt is made to find an available server port, then PROGRAM - is launched with ARGS; the `:autoport' keyword substituted for - that number; and MOREARGS. Eglot then attempts to establish a - TCP connection to that port number on the localhost. + combination of previous options is used. First, an attempt is + made to find an available server port, then PROGRAM is launched + with ARGS; the `:autoport' keyword substituted for that number; + and MOREARGS. Eglot then attempts to establish a TCP + connection to that port number on the localhost. * A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol designating a subclass of `eglot-lsp-server', for representing @@ -627,7 +634,11 @@ treated as in `eglot-dbind'." (cl-defgeneric eglot-initialization-options (server) "JSON object to send under `initializationOptions'." - (:method (_s) eglot--{})) ; blank default + (:method (s) + (let ((probe (plist-get (eglot--saved-initargs s) :initializationOptions))) + (cond ((functionp probe) (funcall probe s)) + (probe) + (t eglot--{}))))) (cl-defgeneric eglot-register-capability (server method id &rest params) "Ask SERVER to register capability METHOD marked with ID." @@ -1116,18 +1127,22 @@ This docstring appeases checkdoc, that's all." (setq autostart-inferior-process inferior) connection)))) ((stringp (car contact)) - `(:process - ,(lambda () - (let ((default-directory default-directory)) - (make-process - :name readable-name - :command (setq server-info (eglot--cmd contact)) - :connection-type 'pipe - :coding 'utf-8-emacs-unix - :noquery t - :stderr (get-buffer-create - (format "*%s stderr*" readable-name)) - :file-handler t))))))) + (let* ((probe (cl-position-if #'keywordp contact)) + (more-initargs (and probe (cl-subseq contact probe))) + (contact (cl-subseq contact 0 probe))) + `(:process + ,(lambda () + (let ((default-directory default-directory)) + (make-process + :name readable-name + :command (setq server-info (eglot--cmd contact)) + :connection-type 'pipe + :coding 'utf-8-emacs-unix + :noquery t + :stderr (get-buffer-create + (format "*%s stderr*" readable-name)) + :file-handler t))) + ,@more-initargs))))) (spread (lambda (fn) (lambda (server method params) (let ((eglot--cached-server server)) (apply fn server method (append params nil)))))) From 1d9542cbe8e4249d185518b746437b83de855810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 31 Mar 2022 14:21:29 +0100 Subject: [PATCH 669/771] Protect against empty firsttriggercharacter strings Which some LS's like gopls like to send. * eglot.el (eglot--post-self-insert-hook): Beware of empty strings. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/906 --- lisp/progmodes/eglot.el | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ade8a7c7118..86e798fabdd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1993,11 +1993,12 @@ THINGS are either registrations or unregisterations (sic)." (setq eglot--last-inserted-char last-input-event) (let ((ot-provider (eglot--server-capable :documentOnTypeFormattingProvider))) (when (and ot-provider - (or (eq last-input-event - (elt (plist-get ot-provider :firstTriggerCharacter) 0)) - (cl-find last-input-event - (plist-get ot-provider :moreTriggerCharacter) - :key #'seq-first))) + (ignore-errors ; github#906, some LS's send empty strings + (or (eq last-input-event + (seq-first (plist-get ot-provider :firstTriggerCharacter))) + (cl-find last-input-event + (plist-get ot-provider :moreTriggerCharacter) + :key #'seq-first)))) (eglot-format (point) nil last-input-event)))) (defun eglot--pre-command-hook () From ef0da9414e148cafef7443660ac90c571850b629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 4 Apr 2022 09:39:59 +0100 Subject: [PATCH 670/771] Make eglot--plist-keys a simple (non-map.el) helper again This removes a nagging compilation warning when developing on Emacs master. There's not much point in depending on map.el just for this util. And there' snot much point in making eglot--plist-keys go through a generic dispatching mechanism when we happen to know the thing being dispatched * eglot.el (eglot--plist-keys): Define in helpers section. --- lisp/progmodes/eglot.el | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 86e798fabdd..f739a0d34db 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1550,6 +1550,9 @@ and just return it. PROMPT shouldn't end with a question mark." "Tell if SERVER's project root is `file-remote-p'." (file-remote-p (project-root (eglot--project server)))) +(defun eglot--plist-keys (plist) "Get keys of a plist." + (cl-loop for (k _v) on plist by #'cddr collect k)) + ;;; Minor modes ;;; @@ -3078,13 +3081,6 @@ If NOERROR, return predicate, else erroring function." (make-obsolete-variable 'eglot--managed-mode-hook 'eglot-managed-mode-hook "1.6") - -(if (< emacs-major-version 27) - (defun eglot--plist-keys (plist) - (cl-loop for (k _v) on plist by #'cddr collect k)) - ;; Make into an obsolete alias once we drop support for Emacs 26. - (defalias 'eglot--plist-keys #'map-keys)) - (provide 'eglot) ;; Local Variables: From 2dad9298d9ba96510fb5f744959284ca46b71968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Sat, 15 Jan 2022 18:51:36 +0100 Subject: [PATCH 671/771] Rework eglot's mode-line Mimic flymake by replacing the old menus of the mode-line with "context menus". List all usefull commands under the main menu (eglot-menu-map), and commands related to LSP debugging under the project menu (eglot-debug-map). * eglot.el (eglot-read-documentation, eglot-customize): New commands. (eglot-mode-line-string): New defcustom. (eglot-menu-map, eglot-debug-map,): New variables. (eglot--mode-line-props): Rework to use eglot-menu-map and eglot-debug-map. (eglot--mode-line-format): Use eglot-mode-line-string. GitHub-reference: close https://github.com/joaotavora/eglot/issues/792 --- lisp/progmodes/eglot.el | 129 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f739a0d34db..c8de62c154b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -322,6 +322,10 @@ let the buffer grow forever." "If non-nil, activate Eglot in cross-referenced non-project files." :type 'boolean) +(defcustom eglot-mode-line-string "eglot" + "String displayed on the mode line when Eglot is active." + :type 'string) + (defvar eglot-withhold-process-id nil "If non-nil, Eglot will not send the Emacs process id to the language server. This can be useful when using docker to run a language server.") @@ -1741,6 +1745,99 @@ If it is activated, also signal textDocument/didOpen." (call-interactively what) (force-mode-line-update t)))))) +(defun eglot-read-documentation () + "Open the on-line documentation." + (interactive) + (browse-url "https://github.com/joaotavora/eglot#readme")) + +(defun eglot-customize () + "Customize Eglot." + (interactive) + (customize-group "eglot")) + +(easy-menu-define eglot-menu-map nil "Eglot" + (let ((action-help + "Get possible code actions for the active region or the point")) + `("Eglot" + ;; Commands for getting information and customization. + ["Read the documentation" eglot-read-documentation + :help "Read the on-line documentation"] + ["Customize Eglot" eglot-customize + :help "Customize Eglot globally"] + "--" + ;; xref like commands. + ["Find definitions" xref-find-definitions + :help "Find definitions of the identifier at point" + :active (eglot--server-capable :definitionProvider)] + ["Find references" xref-find-references + :help "Find references to the identifier at point" + :active (eglot--server-capable :referencesProvider)] + ["Find symbols in workspace (apropos)" xref-find-apropos + :help "Find symbols matching a query" + :active (eglot--server-capable :workspaceSymbolProvider)] + ["Find declaration" eglot-find-declaration + :help "Find declaration for the identifier at point" + :active (eglot--server-capable :declarationProvider)] + ["Find implementation" eglot-find-implementation + :help "Find implementation for the identifier at point" + :active (eglot--server-capable :implementationProvider)] + ["Find type definition" eglot-find-typeDefinition + :help "Find type definition for the identifier at point" + :active (eglot--server-capable :typeDefinitionProvider)] + "--" + ;; LSP-related commands (mostly Eglot's own commands). + ["Rename symbol" eglot-rename + :help "Rename current symbol" + :active (eglot--server-capable :renameProvider)] + ["Format buffer" eglot-format-buffer + :help "Format contents of the buffer" + :active (eglot--server-capable :documentFormattingProvider)] + ["Format region" eglot-format + :help "Format the active region" + :active (and (region-active-p) + (eglot--server-capable :documentRangeFormattingProvider))] + ["Show all diagnostics" flymake-show-buffer-diagnostics + :help "Show diagnostics for current buffer (flymake)"] + ["Show documentation for point" eldoc-doc-buffer + :help "Show documentation for point in a buffer (eldoc)"] + "--" + ;; Code-action commands. + ["All possible code actions" eglot-code-actions + :help ,action-help + :active (eglot--server-capable :codeActionProvider)] + ["Organize imports" eglot-code-action-organize-imports + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)] + ["Extract" eglot-code-action-extract + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)] + ["Inline" eglot-code-action-inline + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)] + ["Rewrite" eglot-code-action-rewrite + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)] + ["Quickfix" eglot-code-action-quickfix + :help ,action-help + :visible (eglot--server-capable :codeActionProvider)]))) + +(easy-menu-define eglot-debug-map nil "Debugging the server communication" + '("Debugging the server communication" + ["Go to events buffer" eglot-events-buffer + :help "Display the log buffer of the server communication"] + ["Go to the stderr buffer" eglot-stderr-buffer + :help "Display the error buffer for current LSP server"] + ["Reconnect to server" eglot-reconnect + :help "Reconnect to the current LSP server"] + ["Quit server" eglot-shutdown + :help "Politely ask the LSP server to quit"] + "--" + ["Customize events buffers" + (lambda () + (interactive) + (customize-variable 'eglot-events-buffer-size)) + :help "Customize variable eglot-events-buffer-size"])) + (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. Uses THING, FACE, DEFS and PREPEND." @@ -1764,18 +1861,28 @@ Uses THING, FACE, DEFS and PREPEND." (`(,_id ,doing ,done-p ,_detail) (and server (eglot--spinner server))) (last-error (and server (jsonrpc-last-error server)))) (append - `(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil)) + `(,(propertize + eglot-mode-line-string + 'face 'eglot-mode-line + 'mouse-face 'mode-line-highlight + 'help-echo "Eglot: an LSP client\nmouse-1: Display minor mode menu" + 'keymap (let ((map (make-sparse-keymap))) + (define-key map [mode-line down-mouse-1] eglot-menu-map) + map))) (when nick - `(":" ,(eglot--mode-line-props - nick 'eglot-mode-line - '((C-mouse-1 eglot-stderr-buffer "go to stderr buffer") - (mouse-1 eglot-events-buffer "go to events buffer") - (mouse-2 eglot-shutdown "quit server") - (mouse-3 eglot-reconnect "reconnect to server"))) - ,@(when last-error + `(":" + ,(propertize + nick + 'face 'eglot-mode-line + 'mouse-face 'mode-line-highlight + 'help-echo (format "Project '%s'\nmouse-1: LSP debugging menu" nick) + 'keymap (let ((map (make-sparse-keymap))) + (define-key map [mode-line down-mouse-1] eglot-debug-map) + map)) + ,@(when last-error `("/" ,(eglot--mode-line-props "error" 'compilation-mode-line-fail - '((mouse-3 eglot-clear-status "clear this status")) + '((mouse-3 eglot-clear-status "Clear this status")) (format "An error occurred: %s\n" (plist-get last-error :message))))) ,@(when (and doing (not done-p)) @@ -1785,9 +1892,9 @@ Uses THING, FACE, DEFS and PREPEND." `("/" ,(eglot--mode-line-props (format "%d" pending) 'warning '((mouse-3 eglot-forget-pending-continuations - "forget pending continuations")) + "Forget pending continuations")) "Number of outgoing, \ -still unanswered LSP requests to the server")))))))) +still unanswered LSP requests to the server\n")))))))) (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) From a63916f92855aa4a0035f60a31d8a0f991194583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 4 Apr 2022 10:23:56 +0100 Subject: [PATCH 672/771] Tweak eglot mode-line menus * eglot.el (eglot-manual): Rename from eglot-read-documentation (eglot-customize): Delete. (eglot-menu): Rename from eglot-menu-map. Rework. (eglot--mode-line-format): Tweak. (eglot-menu-string): Rename from eglot-mode-line-string. (Flymake customization): New source section. * NEWS.md: Tweak. GitHub-reference: per https://github.com/joaotavora/eglot/issues/792 --- lisp/progmodes/eglot.el | 166 +++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 95 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c8de62c154b..6d0b66c2a14 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -322,8 +322,8 @@ let the buffer grow forever." "If non-nil, activate Eglot in cross-referenced non-project files." :type 'boolean) -(defcustom eglot-mode-line-string "eglot" - "String displayed on the mode line when Eglot is active." +(defcustom eglot-menu-string "eglot" + "String displayed in mode line when Eglot is active." :type 'string) (defvar eglot-withhold-process-id nil @@ -1745,98 +1745,71 @@ If it is activated, also signal textDocument/didOpen." (call-interactively what) (force-mode-line-update t)))))) -(defun eglot-read-documentation () - "Open the on-line documentation." - (interactive) - (browse-url "https://github.com/joaotavora/eglot#readme")) +(defun eglot-manual () "Open on-line documentation." + (interactive) (browse-url "https://github.com/joaotavora/eglot#readme")) -(defun eglot-customize () - "Customize Eglot." - (interactive) - (customize-group "eglot")) - -(easy-menu-define eglot-menu-map nil "Eglot" - (let ((action-help - "Get possible code actions for the active region or the point")) - `("Eglot" - ;; Commands for getting information and customization. - ["Read the documentation" eglot-read-documentation - :help "Read the on-line documentation"] - ["Customize Eglot" eglot-customize - :help "Customize Eglot globally"] - "--" - ;; xref like commands. - ["Find definitions" xref-find-definitions - :help "Find definitions of the identifier at point" - :active (eglot--server-capable :definitionProvider)] - ["Find references" xref-find-references - :help "Find references to the identifier at point" - :active (eglot--server-capable :referencesProvider)] - ["Find symbols in workspace (apropos)" xref-find-apropos - :help "Find symbols matching a query" - :active (eglot--server-capable :workspaceSymbolProvider)] - ["Find declaration" eglot-find-declaration - :help "Find declaration for the identifier at point" - :active (eglot--server-capable :declarationProvider)] - ["Find implementation" eglot-find-implementation - :help "Find implementation for the identifier at point" - :active (eglot--server-capable :implementationProvider)] - ["Find type definition" eglot-find-typeDefinition - :help "Find type definition for the identifier at point" - :active (eglot--server-capable :typeDefinitionProvider)] - "--" - ;; LSP-related commands (mostly Eglot's own commands). - ["Rename symbol" eglot-rename - :help "Rename current symbol" - :active (eglot--server-capable :renameProvider)] - ["Format buffer" eglot-format-buffer - :help "Format contents of the buffer" - :active (eglot--server-capable :documentFormattingProvider)] - ["Format region" eglot-format - :help "Format the active region" - :active (and (region-active-p) - (eglot--server-capable :documentRangeFormattingProvider))] - ["Show all diagnostics" flymake-show-buffer-diagnostics - :help "Show diagnostics for current buffer (flymake)"] - ["Show documentation for point" eldoc-doc-buffer - :help "Show documentation for point in a buffer (eldoc)"] - "--" - ;; Code-action commands. - ["All possible code actions" eglot-code-actions - :help ,action-help - :active (eglot--server-capable :codeActionProvider)] - ["Organize imports" eglot-code-action-organize-imports - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)] - ["Extract" eglot-code-action-extract - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)] - ["Inline" eglot-code-action-inline - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)] - ["Rewrite" eglot-code-action-rewrite - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)] - ["Quickfix" eglot-code-action-quickfix - :help ,action-help - :visible (eglot--server-capable :codeActionProvider)]))) - -(easy-menu-define eglot-debug-map nil "Debugging the server communication" - '("Debugging the server communication" - ["Go to events buffer" eglot-events-buffer - :help "Display the log buffer of the server communication"] - ["Go to the stderr buffer" eglot-stderr-buffer - :help "Display the error buffer for current LSP server"] - ["Reconnect to server" eglot-reconnect - :help "Reconnect to the current LSP server"] - ["Quit server" eglot-shutdown - :help "Politely ask the LSP server to quit"] +(easy-menu-define eglot-menu nil "Eglot" + `("Eglot" + ;; Commands for getting information and customization. + ["Read manual" eglot-manual] + ["Customize Eglot" (lambda () (interactive) (customize-group "eglot"))] "--" - ["Customize events buffers" + ;; xref like commands. + ["Find definitions" xref-find-definitions + :help "Find definitions of identifier at point" + :active (eglot--server-capable :definitionProvider)] + ["Find references" xref-find-references + :help "Find references to identifier at point" + :active (eglot--server-capable :referencesProvider)] + ["Find symbols in workspace (apropos)" xref-find-apropos + :help "Find symbols matching a query" + :active (eglot--server-capable :workspaceSymbolProvider)] + ["Find declaration" eglot-find-declaration + :help "Find declaration for identifier at point" + :active (eglot--server-capable :declarationProvider)] + ["Find implementation" eglot-find-implementation + :help "Find implementation for identifier at point" + :active (eglot--server-capable :implementationProvider)] + ["Find type definition" eglot-find-typeDefinition + :help "Find type definition for identifier at point" + :active (eglot--server-capable :typeDefinitionProvider)] + "--" + ;; LSP-related commands (mostly Eglot's own commands). + ["Rename symbol" eglot-rename + :active (eglot--server-capable :renameProvider)] + ["Format buffer" eglot-format-buffer + :active (eglot--server-capable :documentFormattingProvider)] + ["Format active region" eglot-format + :active (and (region-active-p) + (eglot--server-capable :documentRangeFormattingProvider))] + ["Show Flymake diagnostics for buffer" flymake-show-buffer-diagnostics] + ["Show Flymake diagnostics for project" flymake-show-project-diagnostics] + ["Show Eldoc documentation at point" eldoc-doc-buffer] + "--" + ["All possible code actions" eglot-code-actions + :active (eglot--server-capable :codeActionProvider)] + ["Organize imports" eglot-code-action-organize-imports + :visible (eglot--server-capable :codeActionProvider)] + ["Extract" eglot-code-action-extract + :visible (eglot--server-capable :codeActionProvider)] + ["Inline" eglot-code-action-inline + :visible (eglot--server-capable :codeActionProvider)] + ["Rewrite" eglot-code-action-rewrite + :visible (eglot--server-capable :codeActionProvider)] + ["Quickfix" eglot-code-action-quickfix + :visible (eglot--server-capable :codeActionProvider)])) + +(easy-menu-define eglot-server-menu nil "Monitor server communication" + '("Debugging the server communication" + ["Reconnect to server" eglot-reconnect] + ["Quit server" eglot-shutdown] + "--" + ["LSP events buffer" eglot-events-buffer] + ["Server stderr buffer" eglot-stderr-buffer] + ["Customize event buffer size" (lambda () (interactive) - (customize-variable 'eglot-events-buffer-size)) - :help "Customize variable eglot-events-buffer-size"])) + (customize-variable 'eglot-events-buffer-size))])) (defun eglot--mode-line-props (thing face defs &optional prepend) "Helper for function `eglot--mode-line-format'. @@ -1862,12 +1835,12 @@ Uses THING, FACE, DEFS and PREPEND." (last-error (and server (jsonrpc-last-error server)))) (append `(,(propertize - eglot-mode-line-string + eglot-menu-string 'face 'eglot-mode-line 'mouse-face 'mode-line-highlight - 'help-echo "Eglot: an LSP client\nmouse-1: Display minor mode menu" + 'help-echo "Eglot: Emacs LSP client\nmouse-1: Display minor mode menu" 'keymap (let ((map (make-sparse-keymap))) - (define-key map [mode-line down-mouse-1] eglot-menu-map) + (define-key map [mode-line down-mouse-1] eglot-menu) map))) (when nick `(":" @@ -1875,9 +1848,9 @@ Uses THING, FACE, DEFS and PREPEND." nick 'face 'eglot-mode-line 'mouse-face 'mode-line-highlight - 'help-echo (format "Project '%s'\nmouse-1: LSP debugging menu" nick) + 'help-echo (format "Project '%s'\nmouse-1: LSP server control menu" nick) 'keymap (let ((map (make-sparse-keymap))) - (define-key map [mode-line down-mouse-1] eglot-debug-map) + (define-key map [mode-line down-mouse-1] eglot-server-menu) map)) ,@(when last-error `("/" ,(eglot--mode-line-props @@ -1899,6 +1872,9 @@ still unanswered LSP requests to the server\n")))))))) (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) + +;;; Flymake customization +;;; (put 'eglot-note 'flymake-category 'flymake-note) (put 'eglot-warning 'flymake-category 'flymake-warning) (put 'eglot-error 'flymake-category 'flymake-error) From f9cfefcf89cd5d899bd335e9d7674b84cef952f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 4 Apr 2022 10:41:29 +0100 Subject: [PATCH 673/771] Guess language-id if manually entering server program * eglot.el (eglot--guess-contact): Default language-id to educated guess when eglot--lookup-mode returns nil. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/837 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d0b66c2a14..a1c5ab01eb6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -904,7 +904,8 @@ be guessed." (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) (lang-id-and-guess (eglot--lookup-mode guessed-mode)) - (language-id (car lang-id-and-guess)) + (language-id (or (car lang-id-and-guess) + (string-remove-suffix "-mode" (symbol-name guessed-mode)))) (guess (cdr lang-id-and-guess)) (guess (if (functionp guess) (funcall guess interactive) From c17c3cfcbffc97dfae76d75ab32ec81d893be97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 4 Apr 2022 11:05:29 +0100 Subject: [PATCH 674/771] Check textdocumentsync/willsave cap before sending it * eglot.el (eglot--guess-contact): Default language-id to educated guess when eglot--lookup-mode returns nil. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/823 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a1c5ab01eb6..c32560ad8fb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2277,7 +2277,8 @@ When called interactively, use the currently active server" "Send textDocument/willSave to server." (let ((server (eglot--current-server-or-lose)) (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) - (jsonrpc-notify server :textDocument/willSave params) + (when (eglot--server-capable :textDocumentSync :willSave) + (jsonrpc-notify server :textDocument/willSave params)) (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) (ignore-errors (eglot--apply-text-edits From 339ebe7ce467b47ce78404cfae28f29f1bf20ead Mon Sep 17 00:00:00 2001 From: "Billy.Zheng" Date: Tue, 5 Apr 2022 19:45:47 +0800 Subject: [PATCH 675/771] Update invocation for out-of-box dart ls support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot-server-programs): Tweak dart-mode entry. * README.md: Tweak Dart entry. Co-authored-by: João Távora Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/862 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c32560ad8fb..d712b06e76d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -174,7 +174,8 @@ language-server/bin/php-language-server.php")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) (java-mode . ("jdtls")) - (dart-mode . ("dart_language_server")) + (dart-mode . ("dart" "language-server" + "--client-id" "emacs.eglot-dart")) (elixir-mode . ("language_server.sh")) (ada-mode . ("ada_language_server")) (scala-mode . ("metals-emacs")) From 49e56e47d81c0cdfb219d38cb46bfcbdb1c503fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 6 Apr 2022 11:08:12 +0100 Subject: [PATCH 676/771] Solve flymake diagnostics synchronization problems A diagnostics-lazy server is one who doesn't re-report already reported diagnostics when it received textDocument/didSave. Such is the case of Clangd, for example. Before this change, saving an Eglot/Clang-managed buffer with some diagnostics caused the Flymake indicator to display Wait[0 0] until some change was actually done to the buffer. That is because Flymake, by default, wants diagnostics on buffer save, per flymake-start-on-save-buffer. But it doesn't work to simply turn that off. That's because if one types something and quickly saves, and the LSP diagnostics do come in after the save (for some reason, like server latency), then Flymake sometimes doesn't request any diagnostics at all. The reason for the Flymake behaviour wasn't investigated, but that wasn't a very good solution either Rather this change makes it so that when such a Flymake request comes in, it always gets served immediately with the latest information. The latest information is now always stored in eglot--diagnostics, with eglot--unreported-diagnotics being removed. The up-to-date list is reported to Flymake whenever it requests it. It is updated whenever the LSP server decides to. * eglot.el (eglot--last-reported-diagnostics): Delete. (eglot--unreported-diagnostics): Delete. (eglot--diagnostics): New variable.. (eglot--maybe-activate-editing-mode): Use eglot--diagnostics. (eglot-handle-notification): Set eglot--diaggnostics. (eglot-flymake-backend): Read eglot--diagnostics. Always report. (eglot--report-to-flymake): Set eglot--diagnostics. --- lisp/progmodes/eglot.el | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d712b06e76d..52f61e80f4c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1701,8 +1701,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (or (eglot-current-server) (jsonrpc-error "No current JSON-RPC connection"))) -(defvar-local eglot--unreported-diagnostics nil - "Unreported Flymake diagnostics for this buffer.") +(defvar-local eglot--diagnostics nil + "Flymake diagnostics for this buffer.") (defvar revert-buffer-preserve-modes) (defun eglot--after-revert-hook () @@ -1717,7 +1717,7 @@ If it is activated, also signal textDocument/didOpen." ;; Called when `revert-buffer-in-progress-p' is t but ;; `revert-buffer-preserve-modes' is nil. (when (and buffer-file-name (eglot-current-server)) - (setq eglot--unreported-diagnostics `(:just-opened . nil)) + (setq eglot--diagnostics nil) (eglot--managed-mode) (eglot--signal-textDocument/didOpen)))) @@ -1995,7 +1995,7 @@ COMMAND is a symbol naming the command." finally (cond (eglot--current-flymake-report-fn (eglot--report-to-flymake diags)) (t - (setq eglot--unreported-diagnostics (cons t diags)))))) + (setq eglot--diagnostics diags))))) (cl-loop with path = (expand-file-name (eglot--uri-to-path uri)) for diag-spec across diagnostics @@ -2305,9 +2305,7 @@ may be called multiple times (respecting the protocol of `flymake-backend-functions')." (cond (eglot--managed-mode (setq eglot--current-flymake-report-fn report-fn) - ;; Report anything unreported - (when eglot--unreported-diagnostics - (eglot--report-to-flymake (cdr eglot--unreported-diagnostics)))) + (eglot--report-to-flymake eglot--diagnostics)) (t (funcall report-fn nil)))) @@ -2322,7 +2320,7 @@ may be called multiple times (respecting the protocol of ;; keyword forces flymake to delete ;; them (github#159). :region (cons (point-min) (point-max)))) - (setq eglot--unreported-diagnostics nil)) + (setq eglot--diagnostics diags)) (defun eglot-xref-backend () "EGLOT xref backend." 'eglot) From 73f4555a0dab8d5d516febe25162ca8af5aebfad Mon Sep 17 00:00:00 2001 From: Troels Henriksen Date: Fri, 15 Apr 2022 20:22:57 +0200 Subject: [PATCH 677/771] Add out-of-box support for futhark lsp server * eglot.el (eglot-server-programs): Support futhark lsp. * README.md: Update. * NEWS.md: Update. Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/922 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 52f61e80f4c..348ae4b42c9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -187,6 +187,7 @@ language-server/bin/php-language-server.php")) (nix-mode . ("rnix-lsp")) (gdscript-mode . ("localhost" 6008)) ((fortran-mode f90-mode) . ("fortls")) + (futhark-mode . ("futhark" "lsp")) (lua-mode . ("lua-lsp")) (zig-mode . ("zls")) (css-mode . ,(eglot-alternatives '(("vscode-css-language-server" "--stdio") ("css-languageserver" "--stdio")))) From 5e9d78f9cf1ec4da5f24ab32d038d0d67566fbc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 27 Apr 2022 11:11:52 +0100 Subject: [PATCH 678/771] Ensure non-null :settings param in didchangeconfiguration notif * eglot.el (eglot-signal-didChangeConfiguration): Use eglot--{} GitHub-reference: fix https://github.com/joaotavora/eglot/issues/936 --- lisp/progmodes/eglot.el | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 348ae4b42c9..fd82b76c5b9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2198,11 +2198,12 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (cl-loop for (section . v) in eglot-workspace-configuration - collect (if (keywordp section) - section - (intern (format ":%s" section))) - collect v)))) + (or (cl-loop for (section . v) in eglot-workspace-configuration + collect (if (keywordp section) + section + (intern (format ":%s" section))) + collect v) + eglot--{})))) (cl-defmethod eglot-handle-request (server (_method (eql workspace/configuration)) &key items) From f5503420594e0e80a1552edacc8b85b7e29f4223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Ho=C5=82ubowicz?= <45176912+alternateved@users.noreply.github.com> Date: Wed, 27 Apr 2022 13:48:47 +0200 Subject: [PATCH 679/771] Add out-of-box support for purescript lsp server * eglot.el (eglot-server-programs): Support purescript lsp. * README.md: Update. * NEWS.md: Update. Copyright-paperwork-exempt: Yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/905 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fd82b76c5b9..3d1b19c905b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -195,7 +195,8 @@ language-server/bin/php-language-server.php")) (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) (dockerfile-mode . ("docker-langserver" "--stdio")) (clojure-mode . ("clojure-lsp")) - (csharp-mode . ("omnisharp" "-lsp"))) + (csharp-mode . ("omnisharp" "-lsp")) + (purescript-mode . ("purescript-language-server" "--stdio"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific From 4beab004d98f17f830600b4597315f030a7313c3 Mon Sep 17 00:00:00 2001 From: rbrtb <104695105+rbrtb@users.noreply.github.com> Date: Tue, 3 May 2022 09:53:17 +0000 Subject: [PATCH 680/771] Ensure exit-function of eglot-c-at-point runs on exact match When the completion is exact match, exit-function should still run. Say one is using auto-imports feature of pyright. One types foo, and triggers the completion. There are two candidates: foo and foo_bar. If one chooses foo, the status would be 'exact' instead of 'finished', thus exit-function is not executed, foo is not auto-imported. * eglot.el (eglot-completion-at-point): Consider 'exact status. Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/941 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3d1b19c905b..81c545e64f9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2649,7 +2649,7 @@ for which LSP on-type-formatting should be requested." (line-beginning-position)))) :exit-function (lambda (proxy status) - (when (eq status 'finished) + (when (memq status '(finished exact)) ;; To assist in using this whole `completion-at-point' ;; function inside `completion-in-region', ensure the exit ;; function runs in the buffer where the completion was From 29f2ec24713984d561881980aef578faa2a83068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 4 May 2022 21:47:21 +0100 Subject: [PATCH 681/771] Consider diagnostic.code when generating flymake diagnostics Not sure this will please everybody, can almost guess someone is going to ask for a custom switch. Instead this info (and the source) should be passed on to Flymake. That's where the custom switch for controlling formatting of diagnostic messages should exist. But that's too much work right now. * eglot.el (eglot-handle-notification): Consider Diagnostic.code. --- lisp/progmodes/eglot.el | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 81c545e64f9..3a33ad3ec2b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1957,14 +1957,16 @@ COMMAND is a symbol naming the command." (cond ((null sev) 'eglot-error) ((<= sev 1) 'eglot-error) ((= sev 2) 'eglot-warning) - (t 'eglot-note)))) + (t 'eglot-note))) + (mess (source code message) + (concat source (and code (concat " [" code "]")) ": " message))) (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer (cl-loop for diag-spec across diagnostics - collect (eglot--dbind ((Diagnostic) range message severity source tags) + collect (eglot--dbind ((Diagnostic) range code message severity source tags) diag-spec - (setq message (concat source ": " message)) + (setq message (mess source code message)) (pcase-let ((`(,beg . ,end) (eglot--range-region range))) ;; Fallback to `flymake-diag-region' if server @@ -2001,8 +2003,8 @@ COMMAND is a symbol naming the command." (cl-loop with path = (expand-file-name (eglot--uri-to-path uri)) for diag-spec across diagnostics - collect (eglot--dbind ((Diagnostic) range message severity source) diag-spec - (setq message (concat source ": " message)) + collect (eglot--dbind ((Diagnostic) code range message severity source) diag-spec + (setq message (mess source code message)) (let* ((start (plist-get range :start)) (line (1+ (plist-get start :line))) (char (1+ (plist-get start :character)))) From 46a480aa8882d4febc2f8eea42dbd84dff18c0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 9 May 2022 01:17:58 +0100 Subject: [PATCH 682/771] Fix egregious thinko in eglot--uri-to-path One shouldn't unhex the URI before parsing it. Just consider a filename with a # character in it. The character is encoded as C%23, after unhexing the file name becomes. /tmp/C#/Program.cs Now, parsing this as the URL will fail completely as the # mean "anchor" in URLs. * eglot.el (eglot--uri-to-path): Fix thinko. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3a33ad3ec2b..3e3eb3c543a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1440,7 +1440,7 @@ If optional MARKER, return a marker instead" (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) (let* ((server (eglot-current-server)) (remote-prefix (and server (eglot--trampish-p server))) - (retval (url-filename (url-generic-parse-url (url-unhex-string uri)))) + (retval (url-unhex-string (url-filename (url-generic-parse-url uri)))) ;; Remove the leading "/" for local MS Windows-style paths. (normalized (if (and (not remote-prefix) (eq system-type 'windows-nt) From 50ff73d753708467621d6ce8495a5d78dc31ae63 Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Mon, 9 May 2022 21:04:12 +0200 Subject: [PATCH 683/771] Use format string instead of concat * eglot.el (eglot-handle-notification): Because diagnostics code can be integer or string, and integer fails the sequencep test, use format to create this string. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/948 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 3e3eb3c543a..e8f060cd64e 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1959,7 +1959,7 @@ COMMAND is a symbol naming the command." ((= sev 2) 'eglot-warning) (t 'eglot-note))) (mess (source code message) - (concat source (and code (concat " [" code "]")) ": " message))) + (concat source (and code (format " [%s]" code)) ": " message))) (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) (with-current-buffer buffer (cl-loop From 5c6eb3caa903ad1479b3e901cee4b845bada1f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 19 May 2022 09:59:55 +0100 Subject: [PATCH 684/771] Don't ignore flymake-no-changes-timeout Also per https://github.com/joaotavora/eglot/issues/957. Only actually and eagerly report LSP diagnotics if the user has Flymake starting automatically on a timer (flymake-no-changes-timeout is a number). By contrast, if flymake-no-changes-timeout is nil, the user starts the diagnostic collection process on-demand via 'M-x flymake-start'. Since the control of such collection is impossible with LSP, we should just hold on to whatever diagnostics we have (which are presumably up-to-date) until the next invocation of 'eglot-flymake-backend'. For now, this doesn't affect Flymake "list-only" diagnostics. Those are reported via the 'flymake-list-only-diagonstics' variable and are always communicated immediately to it. * eglot.el: (eglot-handle-notification textDocument/publishDiagnostics): Consult flymake-no-changes-timeout. Suggested-by: Jim Davis GitHub-reference: fix https://github.com/joaotavora/eglot/issues/508 --- lisp/progmodes/eglot.el | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e8f060cd64e..4e28de18754 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1996,7 +1996,11 @@ COMMAND is a symbol naming the command." collect it))) `((face . ,faces)))))) into diags - finally (cond (eglot--current-flymake-report-fn + finally (cond ((and + ;; only add to current report if Flymake + ;; starts on idle-timer (github#958) + (not (null flymake-no-changes-timeout)) + eglot--current-flymake-report-fn) (eglot--report-to-flymake diags)) (t (setq eglot--diagnostics diags))))) From f8344871a0159f2550fda3c68207219f1513e1f8 Mon Sep 17 00:00:00 2001 From: Yuan Fu Date: Sun, 12 Jun 2022 03:04:53 -0700 Subject: [PATCH 685/771] Update docstring of eglot-events-buffer-size * eglot.el (eglot-events-buffer-size): Mention that you need to restart the connection for 'eglot-events-buffer-size' to take effect. GitHub-reference: close https://github.com/joaotavora/eglot/issues/974 GitHub-reference: close https://github.com/joaotavora/eglot/issues/776 --- lisp/progmodes/eglot.el | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 4e28de18754..53ae6fae3c3 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -312,7 +312,11 @@ as 0, i.e. don't block at all." "Control the size of the Eglot events buffer. If a number, don't let the buffer grow larger than that many characters. If 0, don't use an event's buffer at all. If nil, -let the buffer grow forever." +let the buffer grow forever. + +For changes on this variable to take effect on a connection +already started, you need to restart the connection. That can be +done by `eglot-reconnect'." :type '(choice (const :tag "No limit" nil) (integer :tag "Number of characters"))) From 68fbcbd6207819b9d653362e66236ea5e04e9b7d Mon Sep 17 00:00:00 2001 From: "Basil L. Contovounesios" Date: Fri, 24 Jun 2022 12:39:02 +0300 Subject: [PATCH 686/771] Reduce memory footprint of eglot--{} * eglot.el (eglot--{}): Specify smallest hash table :size, to spare ~1KiB according to memory-report-object-size. See also https://github.com/joaotavora/eglot/pull/315. GitHub-reference: per https://github.com/joaotavora/eglot/issues/978 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 53ae6fae3c3..bde4a23f8e4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -368,7 +368,7 @@ This can be useful when using docker to run a language server.") `((1 . eglot-diagnostic-tag-unnecessary-face) (2 . eglot-diagnostic-tag-deprecated-face))) -(defconst eglot--{} (make-hash-table) "The empty JSON object.") +(defconst eglot--{} (make-hash-table :size 1) "The empty JSON object.") (defun eglot--executable-find (command &optional remote) "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26." From f8c8c70f8a7ec5649b5dd3f5af559f59eff65724 Mon Sep 17 00:00:00 2001 From: jicksaw Date: Thu, 30 Jun 2022 10:39:33 +0300 Subject: [PATCH 687/771] Reduce eldoc noise from hover messages Also close https://github.com/joaotavora/eglot/issues/985 Only echo hover response content, without response range. LSP specification says the range is meant to visualize a hover. Maybe echoing the range is useful for some, but it seems non-standard behavior. Example issue: haskell-language-server responds with range set to whole file when hovering a comment -> Large, useless eldoc * eglot.el (eglot--hover-info): Remove text selected by range from output Copyright-paperwork-exempt: Yes GitHub-reference: fix https://github.com/joaotavora/eglot/issues/514 --- lisp/progmodes/eglot.el | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index bde4a23f8e4..b058183fb96 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2711,13 +2711,10 @@ for which LSP on-type-formatting should be requested." (eglot--signal-textDocument/didChange) (eldoc))))))))) -(defun eglot--hover-info (contents &optional range) - (let ((heading (and range (pcase-let ((`(,beg . ,end) (eglot--range-region range))) - (concat (buffer-substring beg end) ": ")))) - (body (mapconcat #'eglot--format-markup - (if (vectorp contents) contents (list contents)) "\n"))) - (when (or heading (cl-plusp (length body))) (concat heading body)))) - +(defun eglot--hover-info (contents &optional _range) + (mapconcat #'eglot--format-markup + (if (vectorp contents) contents (list contents)) "\n")) + (defun eglot--sig-info (sigs active-sig sig-help-active-param) (cl-loop for (sig . moresigs) on (append sigs nil) for i from 0 From 9ffcd537f82ecb05996c49014c5b72009134a927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 24 Jun 2022 10:35:07 +0100 Subject: [PATCH 688/771] Apply any additionaltextedits unconditionally * eglot.el (eglot-completion-at-point): Apply any additionalTextEdits unconditionally. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/981 --- lisp/progmodes/eglot.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b058183fb96..eb5b86ed10b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2698,16 +2698,16 @@ for which LSP on-type-formatting should be requested." (eglot--range-region range))) (delete-region beg end) (goto-char beg) - (funcall (or snippet-fn #'insert) newText))) - (when (cl-plusp (length additionalTextEdits)) - (eglot--apply-text-edits additionalTextEdits))) + (funcall (or snippet-fn #'insert) newText)))) (snippet-fn ;; A snippet should be inserted, but using plain ;; `insertText'. This requires us to delete the ;; whole completion, since `insertText' is the full ;; completion's text. (delete-region (- (point) (length proxy)) (point)) - (funcall snippet-fn (or insertText label))))) + (funcall snippet-fn (or insertText label)))) + (when (cl-plusp (length additionalTextEdits)) + (eglot--apply-text-edits additionalTextEdits))) (eglot--signal-textDocument/didChange) (eldoc))))))))) From 6c8aee268d81ef616169d79ea5bd0331aebc25ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 7 Jul 2022 12:30:03 +0100 Subject: [PATCH 689/771] Prevent desktop.el from saving/restoring eglot--managed-mode Although desktop.el compatibility is Emacs bughttps://github.com/joaotavora/eglot/issues/56407, the optimal solution agreed to there is a bit more work than what I have time to right now. See e.g. https://debbugs.gnu.org/cgi/bugreport.cgi?bug=bug%2356407https://github.com/joaotavora/eglot/issues/68. For now, just use `with-eval-after-load' * eglot.el (Hacks desktop): Add eglot--managed-mode to desktop-minor-mode-handlers GitHub-reference: fix https://github.com/joaotavora/eglot/issues/990 --- lisp/progmodes/eglot.el | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eb5b86ed10b..1b9c997d253 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3169,6 +3169,17 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) + +;;; Hacks +;;; +;; FIXME: Although desktop.el compatibility is Emacs bug#56407, the +;; optimal solution agreed to there is a bit more work than what I +;; have time to right now. See +;; e.g. https://debbugs.gnu.org/cgi/bugreport.cgi?bug=bug%2356407#68. +;; For now, just use `with-eval-after-load' +(with-eval-after-load 'desktop + (add-to-list 'desktop-minor-mode-handlers '(eglot--managed-mode . ignore))) + ;;; Obsolete ;;; From 917e8ffa314600b0736ec7117f24ca34bec4e7db Mon Sep 17 00:00:00 2001 From: jgart <47760695+jgarte@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:16:29 -0500 Subject: [PATCH 690/771] Add support for jedi-language-server * eglot.el (eglot-server-programs): Add jedi-language-server * README.md: Mention jedi-language-server * NEWS.md: Mention jedi-language-server Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/961 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1b9c997d253..ebbadea8010 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -151,7 +151,7 @@ chosen (interactively or automatically)." (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode . ,(eglot-alternatives - '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) + '("pylsp" "pyls" ("pyright-langserver" "--stdio") "jedi-language-server"))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) From fd5a5f16d7bb575b0b0323b54028d6a667767519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 13 Jul 2022 00:44:32 +0100 Subject: [PATCH 691/771] Make c-u m-. work half decently * NEWS.md: Mention change. * eglot.el (eglot--lsp-interface-alist): Add WorkspaceSymbol (eglot--workspace-symbols-cache): New variable. (eglot--recover-workspace-meta): New helper. (xref-backend-identifier-completion-table): Complicate. (xref-backend-definitions): Complicate. (completion-category-overrides): Register a category and a style here. (completion-styles-alist): Add eglot--lsp-backend-style style (eglot--lsp-backend-style-call): New funtion. (eglot--lsp-backend-style-all-completions): New function. (eglot--lsp-backend-style-try-completion): New function. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/131 --- lisp/progmodes/eglot.el | 98 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1b9c997d253..b17bfd1b5cd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -417,7 +417,7 @@ This can be useful when using docker to run a language server.") (TextEdit (:range :newText)) (VersionedTextDocumentIdentifier (:uri :version) ()) (WorkspaceEdit () (:changes :documentChanges)) - ) + (WorkspaceSymbol (:name :kind) (:containerName :location :data))) "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. INTERFACE-NAME is a symbol designated by the spec as @@ -2102,7 +2102,7 @@ THINGS are either registrations or unregisterations (sic)." (eglot-format (point) nil last-input-event)))) (defun eglot--pre-command-hook () - "Reset `eglot--last-inserted-char'." + "Reset some temporary variables." (setq eglot--last-inserted-char nil)) (defun eglot--CompletionParams () @@ -2392,8 +2392,53 @@ Try to visit the target file for a richer summary line." (eglot--current-server-or-lose)) (xref-make-match summary (xref-make-file-location file line column) length))) +(defvar eglot--workspace-symbols-cache (make-hash-table :test #'equal) + "Cache of `workspace/Symbol' results used by `xref-find-definitions'.") + (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (eglot--error "Cannot (yet) provide reliable completion table for LSP symbols")) + (if (eglot--server-capable :workspaceSymbolProvider) + (let ((buf (current-buffer))) + (clrhash eglot--workspace-symbols-cache) + (cl-labels ((refresh (pat) + (mapcar + (lambda (wss) + (eglot--dbind ((WorkspaceSymbol) name containerName) wss + (propertize + (concat (and (not (zerop (length containerName))) + (format "%s::" containerName)) + name) + 'eglot--lsp-workspaceSymbol wss))) + (with-current-buffer buf + (jsonrpc-request (eglot--current-server-or-lose) + :workspace/symbol + `(:query ,pat))))) + (lookup (pat) ;; check cache, else refresh + (let* ((cache eglot--workspace-symbols-cache) + (probe (gethash pat cache :missing))) + (if (eq probe :missing) (puthash pat (refresh pat) cache) + probe)))) + (lambda (string _pred action) + (pcase action + (`metadata '(metadata + (display-sort-function . identity) + (category . eglot-indirection-joy))) + (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) + (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) + (_ nil))))) + (eglot--error "This LSP server isn't a :workspaceSymbolProvider"))) + +(defun eglot--recover-workspace-symbol-meta (string) + "Search `eglot--workspace-symbols-cache' for rich entry of STRING." + (catch 'found + (maphash (lambda (_k v) + (while v + ;; Like mess? Ask minibuffer.el about improper lists. + (when (equal (car v) string) (throw 'found (car v))) + (setq v (and (consp v) (cdr v))))) + eglot--workspace-symbols-cache))) + +(add-to-list 'completion-category-overrides + '(eglot-indirection-joy (styles . (eglot--lsp-backend-style)))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) ;; JT@19/10/09: This is a totally dummy identifier that isn't even @@ -2456,8 +2501,14 @@ Try to visit the target file for a richer summary line." (interactive) (eglot--lsp-xref-helper :textDocument/typeDefinition)) -(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) _identifier) - (eglot--lsp-xrefs-for-method :textDocument/definition)) +(cl-defmethod xref-backend-definitions ((_backend (eql eglot)) id) + (let ((probe (eglot--recover-workspace-symbol-meta id))) + (if probe + (eglot--dbind ((WorkspaceSymbol) name location) + (get-text-property 0 'eglot--lsp-workspaceSymbol probe) + (eglot--dbind ((Location) uri range) location + (list (eglot--xref-make-match name uri range)))) + (eglot--lsp-xrefs-for-method :textDocument/definition)))) (cl-defmethod xref-backend-references ((_backend (eql eglot)) _identifier) (or @@ -3188,6 +3239,43 @@ If NOERROR, return predicate, else erroring function." 'eglot-managed-mode-hook "1.6") (provide 'eglot) + +;;; Backend completion + +;; Written by Stefan Monnier circa 2016. Something to move to +;; minibuffer.el "ASAP" (with all the `eglot--lsp-' replaced by +;; something else. The very same code already in SLY and stable for a +;; long time. + +;; This "completion style" delegates all the work to the "programmable +;; completion" table which is then free to implement its own +;; completion style. Typically this is used to take advantage of some +;; external tool which already has its own completion system and +;; doesn't give you efficient access to the prefix completion needed +;; by other completion styles. The table should recognize the symbols +;; 'eglot--lsp-tryc and 'eglot--lsp-allc as ACTION, reply with +;; (eglot--lsp-tryc COMP...) or (eglot--lsp-allc . (STRING . POINT)), +;; accordingly. tryc/allc names made akward/recognizable on purpose. + +(add-to-list 'completion-styles-alist + '(eglot--lsp-backend-style + eglot--lsp-backend-style-try-completion + eglot--lsp-backend-style-all-completions + "Ad-hoc completion style provided by the completion table.")) + +(defun eglot--lsp-backend-style-call (op string table pred point) + (when (functionp table) + (let ((res (funcall table string pred (cons op point)))) + (when (eq op (car-safe res)) + (cdr res))))) + +(defun eglot--lsp-backend-style-try-completion (string table pred point) + (eglot--lsp-backend-style-call 'eglot--lsp-tryc string table pred point)) + +(defun eglot--lsp-backend-style-all-completions (string table pred point) + (eglot--lsp-backend-style-call 'eglot--lsp-allc string table pred point)) + + ;; Local Variables: ;; bug-reference-bug-regexp: "\\(github#\\([0-9]+\\)\\)" ;; bug-reference-url-format: "https://github.com/joaotavora/eglot/issues/%s" From e72fa6d86764027d1c071b73cc83aef8d4d344de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 13 Jul 2022 16:42:10 +0100 Subject: [PATCH 692/771] Experiment with grouping in xref-backend-identifier-completion-table Doesn't look very good. * eglot.el (xref-backend-identifier-completion-table): Add stuff. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 --- lisp/progmodes/eglot.el | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index b17bfd1b5cd..eccd67c1296 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2416,12 +2416,42 @@ Try to visit the target file for a richer summary line." (let* ((cache eglot--workspace-symbols-cache) (probe (gethash pat cache :missing))) (if (eq probe :missing) (puthash pat (refresh pat) cache) - probe)))) + probe))) + (container (c) + (plist-get (get-text-property + 0 'eglot--lsp-workspaceSymbol c) + :containerName))) (lambda (string _pred action) (pcase action - (`metadata '(metadata - (display-sort-function . identity) - (category . eglot-indirection-joy))) + (`metadata `(metadata + (cycle-sort-function + . ,(lambda (completions) + (cl-sort completions + #'string-lessp + :key (lambda (c) + (or (container c) + ""))))) + (category . eglot-indirection-joy) + ;; (annotation-function + ;; . ,(lambda (c) + ;; (plist-get (get-text-property + ;; 0 'eglot--lsp-workspaceSymbol c) + ;; :containerName))) + ;; (affixation-function + ;; . ,(lambda (comps) + ;; (mapcar (lambda (c) + ;; (list c + ;; (plist-get (get-text-property + ;; 0 'eglot--lsp-workspaceSymbol c) + ;; :containerName) + ;; " bla")) + ;; comps))) + (group-function + . ,(lambda (c transformp) + (if (not transformp) + (container c) + c))) + )) (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) (_ nil))))) From b59fa2548e5ec86c3f439fd59ad46abf8840a8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 14 Jul 2022 10:09:27 +0100 Subject: [PATCH 693/771] Cosmetic decisions guaranteed to tick off someone somewhere (tm) The symbols returned by the LSP server must be converted to unique strings if Emacs is to present them in a list. On the other hand, the search operates on the pattern and is completely controlled by the backend. There is not much Eglot, the LSP client, can do about this. Decided to present the unique string to the user, even though it could be hidden. All the manner of :annotation-function, :affixation-function, :group-funcion etc didn't seem to add much value. Grouping was especially useless, since it makes sense to respect the LSP server's account of sorting score, so that better results bubble up to the top. * eglot.el (xref-backend-identifier-completion-table): Uniquify symbols with containerName and kind. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 --- lisp/progmodes/eglot.el | 55 +++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index eccd67c1296..6d3667a84a9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2402,11 +2402,20 @@ Try to visit the target file for a richer summary line." (cl-labels ((refresh (pat) (mapcar (lambda (wss) - (eglot--dbind ((WorkspaceSymbol) name containerName) wss + (eglot--dbind + ((WorkspaceSymbol) name containerName kind) wss (propertize - (concat (and (not (zerop (length containerName))) - (format "%s::" containerName)) - name) + (format "%s%s %s" + (if (zerop (length containerName)) "" + (concat (propertize containerName + 'face 'shadow) + " ")) + name + (propertize (alist-get + kind + eglot--symbol-kind-names + "Unknown") + 'face 'shadow)) 'eglot--lsp-workspaceSymbol wss))) (with-current-buffer buf (jsonrpc-request (eglot--current-server-or-lose) @@ -2417,41 +2426,17 @@ Try to visit the target file for a richer summary line." (probe (gethash pat cache :missing))) (if (eq probe :missing) (puthash pat (refresh pat) cache) probe))) - (container (c) - (plist-get (get-text-property - 0 'eglot--lsp-workspaceSymbol c) - :containerName))) + (score (c) + (cl-getf (get-text-property + 0 'eglot--lsp-workspaceSymbol c) + :score 0))) (lambda (string _pred action) (pcase action (`metadata `(metadata (cycle-sort-function . ,(lambda (completions) - (cl-sort completions - #'string-lessp - :key (lambda (c) - (or (container c) - ""))))) - (category . eglot-indirection-joy) - ;; (annotation-function - ;; . ,(lambda (c) - ;; (plist-get (get-text-property - ;; 0 'eglot--lsp-workspaceSymbol c) - ;; :containerName))) - ;; (affixation-function - ;; . ,(lambda (comps) - ;; (mapcar (lambda (c) - ;; (list c - ;; (plist-get (get-text-property - ;; 0 'eglot--lsp-workspaceSymbol c) - ;; :containerName) - ;; " bla")) - ;; comps))) - (group-function - . ,(lambda (c transformp) - (if (not transformp) - (container c) - c))) - )) + (cl-sort completions #'> :key #'score))) + (category . eglot-indirection-joy))) (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) (_ nil))))) @@ -2475,7 +2460,7 @@ Try to visit the target file for a richer summary line." ;; passed to LSP. The reason for this particular wording is to ;; construct a readable message "No references for LSP identifier at ;; point.". See https://github.com/joaotavora/eglot/issues/314 - "LSP identifier at point.") + "LSP identifier at point") (defvar eglot--lsp-xref-refs nil "`xref' objects for overriding `xref-backend-references''s.") From 9dbc18cbfa5c838453a2036c8d37c673bbc8de1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 15 Jul 2022 10:25:55 +0100 Subject: [PATCH 694/771] Tweak some details, fix some bugs eglot--recover-workspace-symbol-meta had a bug that still made it choke on improper lists. Also, when simply M-. to the thing at point, let's not lose time on iterating a potentially out-of-date eglot--workspace-symbols-cache. So clear it early in the pre-command-hook. * eglot.el (eglot--workspace-symbols-cache): Move up. (eglot--pre-command-hook): Clear eglot--workspace-symbols-cache here. (eglot--recover-workspace-symbol-meta): Check for consp. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 --- lisp/progmodes/eglot.el | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6d3667a84a9..6f9c4f50f21 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2101,8 +2101,12 @@ THINGS are either registrations or unregisterations (sic)." :key #'seq-first)))) (eglot-format (point) nil last-input-event)))) +(defvar eglot--workspace-symbols-cache (make-hash-table :test #'equal) + "Cache of `workspace/Symbol' results used by `xref-find-definitions'.") + (defun eglot--pre-command-hook () "Reset some temporary variables." + (clrhash eglot--workspace-symbols-cache) (setq eglot--last-inserted-char nil)) (defun eglot--CompletionParams () @@ -2392,9 +2396,6 @@ Try to visit the target file for a richer summary line." (eglot--current-server-or-lose)) (xref-make-match summary (xref-make-file-location file line column) length))) -(defvar eglot--workspace-symbols-cache (make-hash-table :test #'equal) - "Cache of `workspace/Symbol' results used by `xref-find-definitions'.") - (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) (if (eglot--server-capable :workspaceSymbolProvider) (let ((buf (current-buffer))) @@ -2446,10 +2447,10 @@ Try to visit the target file for a richer summary line." "Search `eglot--workspace-symbols-cache' for rich entry of STRING." (catch 'found (maphash (lambda (_k v) - (while v + (while (consp v) ;; Like mess? Ask minibuffer.el about improper lists. (when (equal (car v) string) (throw 'found (car v))) - (setq v (and (consp v) (cdr v))))) + (setq v (cdr v)))) eglot--workspace-symbols-cache))) (add-to-list 'completion-category-overrides From b931d93b1549d41eb11a61724e339a4a34b317d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 15 Jul 2022 12:01:44 +0100 Subject: [PATCH 695/771] Guess the "lsp identifier at point" * eglot.el (eglot--workspace-symbols): New helper. (xref-backend-identifier-completion-table): Rework. (xref-backend-identifier-at-point): Rework. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 GitHub-reference: per https://github.com/joaotavora/eglot/issues/314 --- lisp/progmodes/eglot.el | 101 ++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6f9c4f50f21..22eff41f53a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2396,52 +2396,52 @@ Try to visit the target file for a richer summary line." (eglot--current-server-or-lose)) (xref-make-match summary (xref-make-file-location file line column) length))) +(defun eglot--workspace-symbols (pat &optional buffer) + "Ask for :workspace/symbol on PAT, return list of formatted strings. +If BUFFER, switch to it before." + (with-current-buffer (or buffer (current-buffer)) + (unless (eglot--server-capable :workspaceSymbolProvider) + (eglot--error "This LSP server isn't a :workspaceSymbolProvider")) + (mapcar + (lambda (wss) + (eglot--dbind ((WorkspaceSymbol) name containerName kind) wss + (propertize + (format "%s%s %s" + (if (zerop (length containerName)) "" + (concat (propertize containerName 'face 'shadow) " ")) + name + (propertize (alist-get kind eglot--symbol-kind-names "Unknown") + 'face 'shadow)) + 'eglot--lsp-workspaceSymbol wss))) + (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol + `(:query ,pat))))) + (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) - (if (eglot--server-capable :workspaceSymbolProvider) - (let ((buf (current-buffer))) - (clrhash eglot--workspace-symbols-cache) - (cl-labels ((refresh (pat) - (mapcar - (lambda (wss) - (eglot--dbind - ((WorkspaceSymbol) name containerName kind) wss - (propertize - (format "%s%s %s" - (if (zerop (length containerName)) "" - (concat (propertize containerName - 'face 'shadow) - " ")) - name - (propertize (alist-get - kind - eglot--symbol-kind-names - "Unknown") - 'face 'shadow)) - 'eglot--lsp-workspaceSymbol wss))) - (with-current-buffer buf - (jsonrpc-request (eglot--current-server-or-lose) - :workspace/symbol - `(:query ,pat))))) - (lookup (pat) ;; check cache, else refresh - (let* ((cache eglot--workspace-symbols-cache) - (probe (gethash pat cache :missing))) - (if (eq probe :missing) (puthash pat (refresh pat) cache) - probe))) - (score (c) - (cl-getf (get-text-property - 0 'eglot--lsp-workspaceSymbol c) - :score 0))) - (lambda (string _pred action) - (pcase action - (`metadata `(metadata - (cycle-sort-function - . ,(lambda (completions) - (cl-sort completions #'> :key #'score))) - (category . eglot-indirection-joy))) - (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) - (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) - (_ nil))))) - (eglot--error "This LSP server isn't a :workspaceSymbolProvider"))) + "Yet another tricky connection between LSP and Elisp completion semantics." + (let ((buf (current-buffer)) (cache eglot--workspace-symbols-cache)) + (cl-labels ((refresh (pat) (eglot--workspace-symbols pat buf)) + (lookup-1 (pat) ;; check cache, else refresh + (let ((probe (gethash pat cache :missing))) + (if (eq probe :missing) (puthash pat (refresh pat) cache) + probe))) + (lookup (pat) + (let ((res (lookup-1 pat)) + (def (and (string= pat "") (gethash :default cache)))) + (append def res nil))) + (score (c) + (cl-getf (get-text-property + 0 'eglot--lsp-workspaceSymbol c) + :score 0))) + (lambda (string _pred action) + (pcase action + (`metadata `(metadata + (cycle-sort-function + . ,(lambda (completions) + (cl-sort completions #'> :key #'score))) + (category . eglot-indirection-joy))) + (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) + (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) + (_ nil)))))) (defun eglot--recover-workspace-symbol-meta (string) "Search `eglot--workspace-symbols-cache' for rich entry of STRING." @@ -2457,11 +2457,12 @@ Try to visit the target file for a richer summary line." '(eglot-indirection-joy (styles . (eglot--lsp-backend-style)))) (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) - ;; JT@19/10/09: This is a totally dummy identifier that isn't even - ;; passed to LSP. The reason for this particular wording is to - ;; construct a readable message "No references for LSP identifier at - ;; point.". See https://github.com/joaotavora/eglot/issues/314 - "LSP identifier at point") + (let ((attempt + (puthash :default + (ignore-errors + (eglot--workspace-symbols (symbol-name (symbol-at-point)))) + eglot--workspace-symbols-cache))) + (if attempt (car attempt) "LSP identifier at point"))) (defvar eglot--lsp-xref-refs nil "`xref' objects for overriding `xref-backend-references''s.") From 6717589c57edb96c7050df7d33cfcdc805a0eaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 15 Jul 2022 12:58:47 +0100 Subject: [PATCH 696/771] Be more conservative with the lsp identifier guess If the user is not requesting a prompt, opt for the safer approach which is to get the location from textDocument/definition, not from workspace/symbol. Because of things like function overloading, the latter is not always successful in finding exactly the definition of the thing one is invoking M-. on. This requires using an xref-internal symbol, which is kind of unfortunate. * eglot.el (xref-backend-identifier-at-point): Rework. GitHub-reference: per https://github.com/joaotavora/eglot/issues/131 GitHub-reference: per https://github.com/joaotavora/eglot/issues/314 --- lisp/progmodes/eglot.el | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 22eff41f53a..0b64cd2301b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2458,10 +2458,11 @@ If BUFFER, switch to it before." (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) (let ((attempt - (puthash :default - (ignore-errors - (eglot--workspace-symbols (symbol-name (symbol-at-point)))) - eglot--workspace-symbols-cache))) + (and (xref--prompt-p this-command) + (puthash :default + (ignore-errors + (eglot--workspace-symbols (symbol-name (symbol-at-point)))) + eglot--workspace-symbols-cache)))) (if attempt (car attempt) "LSP identifier at point"))) (defvar eglot--lsp-xref-refs nil From 2a12f622dcd69ae7cd2457c8f1fff2b7912dc47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 18 Jul 2022 21:23:32 +0100 Subject: [PATCH 697/771] Eglot-workspace-configuration can be a function * README.md (Workspace configuration): Renamed from per-project configuration. Rework. * NEWS.md: Mention change. * eglot.el (eglot-workspace-configuration): Overhaul. (eglot-signal-didChangeConfiguration): Use new eglot-workspace-configuration. GitHub-reference: per https://github.com/joaotavora/eglot/issues/967 --- lisp/progmodes/eglot.el | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0b64cd2301b..582ad1fdee8 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2199,12 +2199,22 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." (defvar-local eglot-workspace-configuration () "Alist of (SECTION . VALUE) entries configuring the LSP server. -SECTION should be a keyword or a string, value can be anything -that can be converted to JSON.") +SECTION should be a keyword or a string. VALUE is a +plist or a primitive type converted to JSON. + +The value of this variable can also be a unary function of a +`eglot-lsp-server' instance, the server connection requesting the +configuration. It should return an alist of the format described +above.") ;;;###autoload (put 'eglot-workspace-configuration 'safe-local-variable 'listp) +(defun eglot--workspace-configuration (server) + (if (functionp eglot-workspace-configuration) + (funcall eglot-workspace-configuration server) + eglot-workspace-configuration)) + (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. When called interactively, use the currently active server" @@ -2213,7 +2223,7 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (or (cl-loop for (section . v) in eglot-workspace-configuration + (or (cl-loop for (section . v) in (eglot--workspace-configuration server) collect (if (keywordp section) section (intern (format ":%s" section))) @@ -2235,7 +2245,7 @@ When called interactively, use the currently active server" (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) - (alist-get section eglot-workspace-configuration + (alist-get section (eglot--workspace-configuration server) nil nil (lambda (wsection section) (string= From 3c6356b037d0046f23abad9a4c2f1ccdec65e585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 19 Jul 2022 13:55:59 +0100 Subject: [PATCH 698/771] Appease byte-compiler warnings about wrong use of quotes * eglot.el (eglot-stay-out-of, eglot--code-action): Just give it what it wants. --- lisp/progmodes/eglot.el | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 582ad1fdee8..caebced5292 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1596,9 +1596,8 @@ However, if you wish for Eglot to stay out of a particular Emacs facility that you'd like to keep control of add an element to this list and Eglot will refrain from setting it. -For example, to keep your Company customization use - -(add-to-list 'eglot-stay-out-of 'company)") +For example, to keep your Company customization, add the symbol +`company' to this variable.") (defun eglot--stay-out-of-p (symbol) "Tell if EGLOT should stay of of SYMBOL." @@ -3122,7 +3121,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (defmacro eglot--code-action (name kind) "Define NAME to execute KIND code action." `(defun ,name (beg &optional end) - ,(format "Execute '%s' code actions between BEG and END." kind) + ,(format "Execute `%s' code actions between BEG and END." kind) (interactive (eglot--region-bounds)) (eglot-code-actions beg end ,kind))) From 1986c4df88400a319362c87c616a44d0259f12fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 19 Jul 2022 13:57:52 +0100 Subject: [PATCH 699/771] Reply more reasonably to server's workspace/applyedit * eglot.el (eglot-handle-request): Return non-nil (eglot--apply-workspace-edit): Signal jsonrpc-error, not error. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index caebced5292..c3ef543e3dc 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2049,6 +2049,7 @@ THINGS are either registrations or unregisterations (sic)." (_server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit." (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) + `(:applied t)) (cl-defmethod eglot-handle-request (server (_method (eql workspace/workspaceFolders))) @@ -3025,7 +3026,7 @@ for which LSP on-type-formatting should be requested." (unless (y-or-n-p (format "[eglot] Server wants to edit:\n %s\n Proceed? " (mapconcat #'identity (mapcar #'car prepared) "\n "))) - (eglot--error "User cancelled server edit"))) + (jsonrpc-error "User cancelled server edit"))) (cl-loop for edit in prepared for (path edits version) = edit do (with-current-buffer (find-file-noselect path) From b6e041a24be538758727462f042822a8a1d285c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 19 Jul 2022 17:50:35 +0100 Subject: [PATCH 700/771] Fix embarrassing paren-matching blunder in eglot.el * eglot.el (eglot-handle-request workspace/applyEdit): Fix parens. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c3ef543e3dc..f9a7d2d1e76 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2048,7 +2048,7 @@ THINGS are either registrations or unregisterations (sic)." (cl-defmethod eglot-handle-request (_server (_method (eql workspace/applyEdit)) &key _label edit) "Handle server request workspace/applyEdit." - (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits)) + (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits) `(:applied t)) (cl-defmethod eglot-handle-request From cc5d1a5a72ccceaecc6ef1c3eb905481802a3b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Fri, 22 Jul 2022 01:17:05 +0100 Subject: [PATCH 701/771] Always default eglot-strict-mode to nil it's mostly useful for developers/debugger. It's better to have the latter remember to set it than users being hindered by it. See https://github.com/joaotavora/eglot/issues/131#issuecomment-1191997167 * eglot.el (eglot-strict-mode): default to nil. --- lisp/progmodes/eglot.el | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f9a7d2d1e76..2e332c470f9 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -433,14 +433,12 @@ Here's what an element of this alist might look like: (Command ((:title . string) (:command . string)) (:arguments))")) (eval-and-compile - (defvar eglot-strict-mode (if load-file-name '() - '(disallow-non-standard-keys - ;; Uncomment these two for fun at - ;; compile-time or with flymake-mode. - ;; - ;; enforce-required-keys - ;; enforce-optional-keys - )) + (defvar eglot-strict-mode + '(;; Uncomment next lines for fun and debugging + ;; disallow-non-standard-keys + ;; enforce-required-keys + ;; enforce-optional-keys + ) "How strictly to check LSP interfaces at compile- and run-time. Value is a list of symbols (if the list is empty, no checks are From e74e19991213d1298afb22be6af356c91de2a87a Mon Sep 17 00:00:00 2001 From: Christian Garbs Date: Tue, 26 Jul 2022 16:05:46 +0200 Subject: [PATCH 702/771] Add out-of-box support for perl lsp server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eglot.el (eglot-server-programs): Support Perl lsp. * README.md: Update. * NEWS.md: Update. Co-authored-by: João Távora GitHub-reference: close https://github.com/joaotavora/eglot/issues/952 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2e332c470f9..14e7980d380 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -196,7 +196,8 @@ language-server/bin/php-language-server.php")) (dockerfile-mode . ("docker-langserver" "--stdio")) (clojure-mode . ("clojure-lsp")) (csharp-mode . ("omnisharp" "-lsp")) - (purescript-mode . ("purescript-language-server" "--stdio"))) + (purescript-mode . ("purescript-language-server" "--stdio")) + (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific From 6ee995fe6bea311147b92aa35051e9a7574fa2a9 Mon Sep 17 00:00:00 2001 From: Artem Pyanykh Date: Thu, 8 Sep 2022 11:36:07 +0100 Subject: [PATCH 703/771] Add marksman server for markdown * eglot.el (eglot-server-programs): Update. * README (Connecting to a server): Add marksman. * NEWS.md: Mention change. Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/1013 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 14e7980d380..a04a4f762d4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -197,7 +197,8 @@ language-server/bin/php-language-server.php")) (clojure-mode . ("clojure-lsp")) (csharp-mode . ("omnisharp" "-lsp")) (purescript-mode . ("purescript-language-server" "--stdio")) - (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run"))) + (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run")) + (markdown-mode . ("marksman" "server"))) "How the command `eglot' guesses the server to start. An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE identifies the buffers that are to be managed by a specific From e5b021c01fceea02b7e6622cde0a347b842ca6f3 Mon Sep 17 00:00:00 2001 From: Manuel Uberti Date: Thu, 8 Sep 2022 10:36:56 +0000 Subject: [PATCH 704/771] Fix jdtls support PR https://github.com/joaotavora/eglot/issues/1026 * eglot.el (eglot-server-programs): Add -data setup for java-mode. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1008 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index a04a4f762d4..80884903636 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -173,7 +173,7 @@ language-server/bin/php-language-server.php")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) - (java-mode . ("jdtls")) + (java-mode . ("jdtls" "-data" ".jdtls-cache")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) (elixir-mode . ("language_server.sh")) From 41a42e631bd798151130097feafa6f535161b9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 8 Sep 2022 11:53:11 +0100 Subject: [PATCH 705/771] Don't return poorly supported "special elements" in eglot-imenu Fix https://github.com/joaotavora/eglot/issues/758, https://github.com/joaotavora/eglot/issues/536, https://github.com/joaotavora/eglot/issues/535. Eglot's eglot-imenu returned a structure compliant with the rules outlined in imenu--index-alist. In particular, it returned some elements of the form (INDEX-NAME POSITION GOTO-FN ARGUMENTS...) The original intention (mine) must have been to allow fancy highlighting of the position navigated to with a custom GOTO-FN. Not only was access to that fanciness never implemented, but many other imenu frontends do not support such elements. See for example https://github.com/joaotavora/eglot/issues/758, https://github.com/joaotavora/eglot/issues/536, https://github.com/joaotavora/eglot/issues/535. And also related issues in other packages: https://github.com/IvanMalison/flimenu/issues/6 https://github.com/bmag/imenu-list/issues/58 So it's best to remove this problematic feature for now. It can be added back later. * eglot.el (eglot-imenu): Simplify. * NEWS.md: Mention change --- lisp/progmodes/eglot.el | 52 ++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 80884903636..2ac9b0dff66 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2916,39 +2916,37 @@ for which LSP on-type-formatting should be requested." nil))) (defun eglot-imenu () - "EGLOT's `imenu-create-index-function'." + "EGLOT's `imenu-create-index-function'. +Returns a list as described in docstring of `imenu--index-alist'." (cl-labels - ((visit (_name one-obj-array) - (imenu-default-goto-function - nil (car (eglot--range-region - (eglot--dcase (aref one-obj-array 0) - (((SymbolInformation) location) - (plist-get location :range)) - (((DocumentSymbol) selectionRange) - selectionRange)))))) - (unfurl (obj) - (eglot--dcase obj - (((SymbolInformation)) (list obj)) - (((DocumentSymbol) name children) - (cons obj - (mapcar - (lambda (c) - (plist-put - c :containerName - (let ((existing (plist-get c :containerName))) - (if existing (format "%s::%s" name existing) - name)))) - (mapcan #'unfurl children))))))) + ((unfurl (obj) + (eglot--dcase obj + (((SymbolInformation)) (list obj)) + (((DocumentSymbol) name children) + (cons obj + (mapcar + (lambda (c) + (plist-put + c :containerName + (let ((existing (plist-get c :containerName))) + (if existing (format "%s::%s" name existing) + name)))) + (mapcan #'unfurl children))))))) (mapcar (pcase-lambda (`(,kind . ,objs)) (cons (alist-get kind eglot--symbol-kind-names "Unknown") (mapcan (pcase-lambda (`(,container . ,objs)) - (let ((elems (mapcar (lambda (obj) - (list (plist-get obj :name) - `[,obj] ;; trick - #'visit)) - objs))) + (let ((elems (mapcar + (lambda (obj) + (cons (plist-get obj :name) + (car (eglot--range-region + (eglot--dcase obj + (((SymbolInformation) location) + (plist-get location :range)) + (((DocumentSymbol) selectionRange) + selectionRange)))))) + objs))) (if container (list (cons container elems)) elems))) (seq-group-by (lambda (e) (plist-get e :containerName)) objs)))) From dd017359e974ff2bbbb0db8ceba8e13de6035900 Mon Sep 17 00:00:00 2001 From: jgart <47760695+jgarte@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:16:29 -0500 Subject: [PATCH 706/771] Add support for jedi-language-server (again) * eglot.el (eglot-server-programs): Add jedi-language-server * README.md: Mention jedi-language-server * NEWS.md: Mention jedi-language-server Copyright-paperwork-exempt: yes GitHub-reference: close https://github.com/joaotavora/eglot/issues/961 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 2ac9b0dff66..80f0b654706 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -151,7 +151,7 @@ chosen (interactively or automatically)." (vimrc-mode . ("vim-language-server" "--stdio")) (python-mode . ,(eglot-alternatives - '("pylsp" "pyls" ("pyright-langserver" "--stdio")))) + '("pylsp" "pyls" ("pyright-langserver" "--stdio") "jedi-language-server"))) ((js-mode typescript-mode) . ("typescript-language-server" "--stdio")) (sh-mode . ("bash-language-server" "start")) From d2e842bbf51134bfd33943247faa88db99a36842 Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Fri, 9 Sep 2022 23:25:50 +0200 Subject: [PATCH 707/771] Prefer documentchanges to changes in server-initiated edits Some servers return both. PR: https://github.com/joaotavora/eglot/issues/949 * eglot.el (eglot--apply-workspace-edit): When both documentChanges and changes are present, prefer the documentChanges. By doing that we ensure that we don't double edit, rendering the document in an unusable state. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/704 --- lisp/progmodes/eglot.el | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 80f0b654706..e399b29f09f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3016,8 +3016,12 @@ Returns a list as described in docstring of `imenu--index-alist'." textDocument (list (eglot--uri-to-path uri) edits version))) documentChanges))) - (cl-loop for (uri edits) on changes by #'cddr - do (push (list (eglot--uri-to-path uri) edits) prepared)) + (unless (and changes documentChanges) + ;; We don't want double edits, and some servers send both + ;; changes and documentChanges. This unless ensures that we + ;; prefer documentChanges over changes. + (cl-loop for (uri edits) on changes by #'cddr + do (push (list (eglot--uri-to-path uri) edits) prepared))) (if (or confirm (cl-notevery #'find-buffer-visiting (mapcar #'car prepared))) From 51ae66b50c9f73956039fddba72dbd7213926622 Mon Sep 17 00:00:00 2001 From: Fredrik Bergroth Date: Mon, 10 Jan 2022 15:09:36 +0100 Subject: [PATCH 708/771] Add eglot-show-configuration to debug workspace configurations Also see https://github.com/joaotavora/eglot/issues/790, https://github.com/joaotavora/eglot/issues/1033. GitHub-reference: per https://github.com/joaotavora/eglot/issues/590 --- lisp/progmodes/eglot.el | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index e399b29f09f..91733a8d7a6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -75,6 +75,7 @@ (require 'filenotify) (require 'ert) (require 'array) +(require 'json) ;; ElDoc is preloaded in Emacs, so `require'-ing won't guarantee we are ;; using the latest version from GNU Elpa when we load eglot.el. Use an @@ -2210,6 +2211,18 @@ above.") ;;;###autoload (put 'eglot-workspace-configuration 'safe-local-variable 'listp) +(defun eglot-show-configuration (server) + "Dump `eglot-workspace-configuration' as json for debugging." + (interactive (list (eglot--read-server "Server configuration" + (eglot-current-server)))) + (let ((conf (eglot--workspace-configuration server))) + (with-current-buffer (get-buffer-create " *eglot configuration*") + (erase-buffer) + (insert (jsonrpc--json-encode conf)) + (json-mode) + (json-pretty-print-buffer) + (pop-to-buffer (current-buffer))))) + (defun eglot--workspace-configuration (server) (if (functionp eglot-workspace-configuration) (funcall eglot-workspace-configuration server) From e5f77f8ca59b899bcc2b77e8e8eb27a2749a9130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 17 Sep 2022 01:28:52 +0100 Subject: [PATCH 709/771] Rework readme.md about workspace configuration again Also tweak eglot-show-workspace-configuration a bit. * README.md (Workspace configuration): Rework. * eglot.el (eglot-show-workspace-configuration): Rework. (eglot--workspace-configuration-plist): New helper. GitHub-reference: per https://github.com/joaotavora/eglot/issues/590 --- lisp/progmodes/eglot.el | 43 ++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 91733a8d7a6..f02ec73043c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -58,7 +58,6 @@ ;;; Code: -(require 'json) (require 'imenu) (require 'cl-lib) (require 'project) @@ -2204,23 +2203,28 @@ SECTION should be a keyword or a string. VALUE is a plist or a primitive type converted to JSON. The value of this variable can also be a unary function of a -`eglot-lsp-server' instance, the server connection requesting the -configuration. It should return an alist of the format described -above.") +single argument, which will be a connected `eglot-lsp-server' +instance. The function runs with `default-directory' set to the +root of the current project. It should return an alist of the +format described above.") ;;;###autoload (put 'eglot-workspace-configuration 'safe-local-variable 'listp) -(defun eglot-show-configuration (server) - "Dump `eglot-workspace-configuration' as json for debugging." - (interactive (list (eglot--read-server "Server configuration" - (eglot-current-server)))) - (let ((conf (eglot--workspace-configuration server))) - (with-current-buffer (get-buffer-create " *eglot configuration*") +(defun eglot-show-workspace-configuration (&optional server) + "Dump `eglot-workspace-configuration' as JSON for debugging." + (interactive (list (and (eglot-current-server) + (eglot--read-server "Server configuration" + (eglot-current-server))))) + (let ((conf (eglot--workspace-configuration-plist server))) + (with-current-buffer (get-buffer-create "*EGLOT workspace configuration*") (erase-buffer) (insert (jsonrpc--json-encode conf)) - (json-mode) - (json-pretty-print-buffer) + (with-no-warnings + (require 'json) + (require 'json-mode) + (json-mode) + (json-pretty-print-buffer)) (pop-to-buffer (current-buffer))))) (defun eglot--workspace-configuration (server) @@ -2228,6 +2232,14 @@ above.") (funcall eglot-workspace-configuration server) eglot-workspace-configuration)) +(defun eglot--workspace-configuration-plist (server) + "Returns `eglot-workspace-configuraiton' suitable serialization." + (or (cl-loop for (section . v) in (eglot--workspace-configuration server) + collect (if (keywordp section) section + (intern (format ":%s" section))) + collect v) + eglot--{})) + (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. When called interactively, use the currently active server" @@ -2236,12 +2248,7 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (or (cl-loop for (section . v) in (eglot--workspace-configuration server) - collect (if (keywordp section) - section - (intern (format ":%s" section))) - collect v) - eglot--{})))) + (eglot--workspace-configuration-plist server)))) (cl-defmethod eglot-handle-request (server (_method (eql workspace/configuration)) &key items) From bef332a98314c5cbd76cce79ee0a60cb6aa94172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 17 Sep 2022 10:33:26 +0100 Subject: [PATCH 710/771] Adjust last commit about workspace configuration * README.md (way): Adjust. * eglot.el (json): Don't require needlessly. (eglot-show-workspace-configuration): Don't depend on json-mode. GitHub-reference: per https://github.com/joaotavora/eglot/issues/790 GitHub-reference: per https://github.com/joaotavora/eglot/issues/590 --- lisp/progmodes/eglot.el | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index f02ec73043c..ff94d5ca5f4 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -74,7 +74,6 @@ (require 'filenotify) (require 'ert) (require 'array) -(require 'json) ;; ElDoc is preloaded in Emacs, so `require'-ing won't guarantee we are ;; using the latest version from GNU Elpa when we load eglot.el. Use an @@ -2222,8 +2221,7 @@ format described above.") (insert (jsonrpc--json-encode conf)) (with-no-warnings (require 'json) - (require 'json-mode) - (json-mode) + (when (require 'json-mode nil t) (json-mode)) (json-pretty-print-buffer)) (pop-to-buffer (current-buffer))))) From 523547321e4caca6fc966bd71ecd7b60a6e98f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 17 Sep 2022 21:40:34 +0100 Subject: [PATCH 711/771] Allow :initializationoptions in eglot-server-programs Also see https://github.com/joaotavora/eglot/issues/1038. This feature was poorly tested, and simply wouldn't work when trying to initialize the server object. The simple solution is to ignore :initializationOptions initarg in this context. It is still stored separately as and accessed as the 'eglot--saved-initargs' slot. Another complication arises in eglot--guess-contact, which tried too hard to be able to compose an interactive prompt (when the server program can't be found). The solution is just to give up when :autoport or :initializationOptions is found. It's not easy or practical to have the user provide non-string arguments via a string interface like the minibuffer. * eglot.el (initialize-instance :before eglot-lsp-server): Don't pass :initializationOptions initarg onward. (eglot--guess-contact): Simplify. Don't try heroics with :autoport and :initializationOptions. * eglot-tests.el (eglot-server-programs-simple-missing-executable): Update test. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/940 --- lisp/progmodes/eglot.el | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index ff94d5ca5f4..493bfcc7d6c 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -796,6 +796,9 @@ treated as in `eglot-dbind'." :documentation "Represents a server. Wraps a process for LSP communication.") +(cl-defmethod initialize-instance :before ((_server eglot-lsp-server) &optional args) + (cl-remf args :initializationOptions)) + ;;; Process management (defvar eglot--servers-by-project (make-hash-table :test #'equal) @@ -929,10 +932,10 @@ be guessed." (base-prompt (and interactive "Enter program to execute (or :): ")) - (program-guess + (full-program-invocation (and program - (combine-and-quote-strings (cl-subst ":autoport:" - :autoport guess)))) + (cl-every #'stringp guess) + (combine-and-quote-strings guess))) (prompt (and base-prompt (cond (current-prefix-arg base-prompt) @@ -942,25 +945,23 @@ be guessed." ((and program (not (file-name-absolute-p program)) (not (eglot--executable-find program t))) - (concat (format "[eglot] I guess you want to run `%s'" - program-guess) - (format ", but I can't find `%s' in PATH!" program) - "\n" base-prompt))))) + (if full-program-invocation + (concat (format "[eglot] I guess you want to run `%s'" + full-program-invocation) + (format ", but I can't find `%s' in PATH!" + program) + "\n" base-prompt) + (eglot--error + (concat "`%s' not found in PATH, but can't form" + " an interactive prompt for to fix %s!") + program guess)))))) (contact (or (and prompt - (let ((s (read-shell-command - prompt - program-guess - 'eglot-command-history))) - (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$" - (string-trim s)) - (list (match-string 1 s) - (string-to-number (match-string 2 s))) - (cl-subst - :autoport ":autoport:" (split-string-and-unquote s) - :test #'equal)))) - guess - (eglot--error "Couldn't guess for `%s'!" managed-mode)))) + (read-shell-command + prompt + full-program-invocation + 'eglot-command-history)) + guess))) (list managed-mode (eglot--current-project) class contact language-id))) (defvar eglot-lsp-context) From 14586fedcf9ac4aafe119b89a72d0b438f6a4d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 18 Sep 2022 10:15:43 +0100 Subject: [PATCH 712/771] Don't exceed max-specdl-size in big go projects When invoking client/registerCapability for workspace/didChangeWatchedFiles, Gopls lists each file to watch separately. This makes eglot--glob-emit-{} emit a closure with an 'or' form containing a potentially large number of 're-search-forward' forms. For large Go project such as "Kubernetes", this list becomes so large that -- for some reason I don't understand -- it triggers the 'max-specdl-size' limit. An alternative using `regexp` opt doesn't seem to trigger the error. * eglot.el (eglot--glob-emit-{}): Use regexp-opt. GitHub-reference: fix https://github.com/joaotavora/eglot/issues/633 GitHub-reference: fix https://github.com/joaotavora/eglot/issues/1067 --- lisp/progmodes/eglot.el | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 493bfcc7d6c..038847c78f6 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3260,8 +3260,7 @@ If NOERROR, return predicate, else erroring function." (defun eglot--glob-emit-{} (arg self next) (let ((alternatives (split-string (substring arg 1 (1- (length arg))) ","))) `(,self () - (or ,@(cl-loop for alt in alternatives - collect `(re-search-forward ,(concat "\\=" alt) nil t)) + (or (re-search-forward ,(concat "\\=" (regexp-opt alternatives)) nil t) (error "Failed matching any of %s" ',alternatives)) (,next)))) From a5983527506d0d79137003dc9b5eb15dfd4c7365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 18 Sep 2022 00:37:31 +0100 Subject: [PATCH 713/771] Allow eglot-workspace-configuration to be a plist Suggested-by: Augusto Stoffel * NEWS.md: Mention change. * README.md (eglot-workspace-configuration): Update yet again. Update examples to use pylsp. * eglot.el (eglot--workspace-configuration-plist): Noop if already a plist. (eglot-handle-request workspace/configuration): Use eglot--workspace-configuration-plist. (eglot-workspace-configuration): Document variable. GitHub-reference: per https://github.com/joaotavora/eglot/issues/590 GitHub-reference: per https://github.com/joaotavora/eglot/issues/790 GitHub-reference: per https://github.com/joaotavora/eglot/issues/1033 --- lisp/progmodes/eglot.el | 46 +++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 038847c78f6..c2db7e817f2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2198,14 +2198,32 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." '((name . eglot--signal-textDocument/didChange))) (defvar-local eglot-workspace-configuration () - "Alist of (SECTION . VALUE) entries configuring the LSP server. -SECTION should be a keyword or a string. VALUE is a -plist or a primitive type converted to JSON. + "Configure LSP servers specifically for a given project. + +This variable's value should be a plist (SECTION VALUE ...). +SECTION is a keyword naming a parameter section relevant to a +particular server. VALUE is a plist or a primitive type +converted to JSON also understood by that server. + +Instead of a plist, an alist ((SECTION . VALUE) ...) can be used +instead, but this variant is less reliable and not recommended. + +This variable should be set as a directory-local variable. See +See info node `(emacs)Directory Variables' for various ways to to +that. + +Here's an example value that establishes two sections relevant to +the Pylsp and Gopls LSP servers: + + (:pylsp (:plugins (:jedi_completion (:include_params t + :fuzzy t) + :pylint (:enabled :json-false))) + :gopls (:usePlaceholders t)) The value of this variable can also be a unary function of a single argument, which will be a connected `eglot-lsp-server' instance. The function runs with `default-directory' set to the -root of the current project. It should return an alist of the +root of the current project. It should return an object of the format described above.") ;;;###autoload @@ -2232,12 +2250,15 @@ format described above.") eglot-workspace-configuration)) (defun eglot--workspace-configuration-plist (server) - "Returns `eglot-workspace-configuraiton' suitable serialization." - (or (cl-loop for (section . v) in (eglot--workspace-configuration server) - collect (if (keywordp section) section - (intern (format ":%s" section))) - collect v) - eglot--{})) + "Returns `eglot-workspace-configuration' suitable for serialization." + (let ((val (eglot--workspace-configuration server))) + (or (and (consp (car val)) + (cl-loop for (section . v) in val + collect (if (keywordp section) section + (intern (format ":%s" section))) + collect v)) + val + eglot--{}))) (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. @@ -2264,9 +2285,8 @@ When called interactively, use the currently active server" (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) - (alist-get section (eglot--workspace-configuration server) - nil nil - (lambda (wsection section) + (plist-get (eglot--workspace-configuration-plist server) section + (lambda (section wsection) (string= (if (keywordp wsection) (substring (symbol-name wsection) 1) From ec7d63cbe75b2de4a8e85852f077c7ab04d87524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sun, 18 Sep 2022 11:19:00 +0100 Subject: [PATCH 714/771] Don't return hash tables from e-w-configuration-plist * eglot.el (eglot-signal-didChangeConfiguration): Adjust. (eglot-handle-request workspace-configuration): Adjust. (eglot--workspace-configuration-plist): Don't return a hashtable. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1033 --- lisp/progmodes/eglot.el | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index c2db7e817f2..7c1e849389d 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2257,8 +2257,7 @@ format described above.") collect (if (keywordp section) section (intern (format ":%s" section))) collect v)) - val - eglot--{}))) + val))) (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. @@ -2268,7 +2267,8 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (eglot--workspace-configuration-plist server)))) + (or (eglot--workspace-configuration-plist server) + eglot--{})))) (cl-defmethod eglot-handle-request (server (_method (eql workspace/configuration)) &key items) From 68b9c03b44a056e7a1454879b7f1b9cf050f7ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 19 Sep 2022 16:04:31 +0100 Subject: [PATCH 715/771] Don't use three-argument plist-get * eglot.el (eglot-handle-request): Don't use three-argument plist-get. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1024 --- lisp/progmodes/eglot.el | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 7c1e849389d..0867e43e07f 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2285,13 +2285,15 @@ When called interactively, use the currently active server" (project-root (eglot--project server))))) (setq-local major-mode (eglot--major-mode server)) (hack-dir-local-variables-non-file-buffer) - (plist-get (eglot--workspace-configuration-plist server) section - (lambda (section wsection) - (string= - (if (keywordp wsection) - (substring (symbol-name wsection) 1) - wsection) - section)))))) + (cl-loop for (wsection o) + on (eglot--workspace-configuration-plist server) + by #'cddr + when (string= + (if (keywordp wsection) + (substring (symbol-name wsection) 1) + wsection) + section) + return o)))) items))) (defun eglot--signal-textDocument/didChange () From 0829d5e7a2e7a958cb55df8cbc983bfcf7d4b500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 19 Sep 2022 16:19:33 +0100 Subject: [PATCH 716/771] Revert "fix jdtls support" This reverts commit e5b021c01fceea02b7e6622cde0a347b842ca6f3. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1008 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0867e43e07f..718a42dbd75 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -172,7 +172,7 @@ language-server/bin/php-language-server.php")) (go-mode . ("gopls")) ((R-mode ess-r-mode) . ("R" "--slave" "-e" "languageserver::run()")) - (java-mode . ("jdtls" "-data" ".jdtls-cache")) + (java-mode . ("jdtls")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) (elixir-mode . ("language_server.sh")) From b2054790358ec89c3c3e0e79afda4adbce7f1dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 24 Sep 2022 09:54:04 +0100 Subject: [PATCH 717/771] Fix blunder in eglot--guess-contact * eglot.el (eglot--guess-contact): Add back 'split-string-and-unquote' lost in https://github.com/joaotavora/eglot/issues/940 fix. GitHub-reference: per https://github.com/joaotavora/eglot/issues/940 --- lisp/progmodes/eglot.el | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 718a42dbd75..650b4cccccb 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -957,10 +957,11 @@ be guessed." program guess)))))) (contact (or (and prompt - (read-shell-command - prompt - full-program-invocation - 'eglot-command-history)) + (split-string-and-unquote + (read-shell-command + prompt + full-program-invocation + 'eglot-command-history))) guess))) (list managed-mode (eglot--current-project) class contact language-id))) From f06a837f291004e4d25804d78dd8ae17eba2fe14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 26 Sep 2022 10:23:53 +0100 Subject: [PATCH 718/771] Shoosh byte-compilation warnings about line numbering functions Also add warning at the top of file about not using functionality incompatible with 26.3 * eglot.el: (eglot-current-column, eglot-current-column): Use line-beginning-position (eglot--xref-make-match): Use line-beginning-position, line-end-position, line-number-at-pos --- lisp/progmodes/eglot.el | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 650b4cccccb..fa29c606179 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -9,6 +9,10 @@ ;; Keywords: convenience, languages ;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0") (seq "2.23")) +;; This is (or will soon) be a GNU ELPA :core package. Avoid using +;; functionality that not compatible with the version of Emacs +;; recorded above. + ;; This file is part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify @@ -1336,7 +1340,7 @@ CONNECT-ARGS are passed as additional arguments to (let ((warning-minimum-level :error)) (display-warning 'eglot (apply #'format format args) :warning))) -(defun eglot-current-column () (- (point) (point-at-bol))) +(defun eglot-current-column () (- (point) (line-beginning-position))) (defvar eglot-current-column-function #'eglot-lsp-abiding-column "Function to calculate the current column. @@ -1985,10 +1989,10 @@ COMMAND is a symbol naming the command." (eglot--widening (goto-char (point-min)) (setq beg - (point-at-bol + (line-beginning-position (1+ (plist-get (plist-get range :start) :line)))) (setq end - (point-at-eol + (line-end-position (1+ (plist-get (plist-get range :end) :line))))))) (eglot--make-diag (current-buffer) beg end @@ -2422,14 +2426,14 @@ Try to visit the target file for a richer summary line." (collect (lambda () (eglot--widening (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) - (bol (progn (goto-char beg) (point-at-bol))) - (substring (buffer-substring bol (point-at-eol))) + (bol (progn (goto-char beg) (line-beginning-position))) + (substring (buffer-substring bol (line-end-position))) (hi-beg (- beg bol)) - (hi-end (- (min (point-at-eol) end) bol))) + (hi-end (- (min (line-end-position) end) bol))) (add-face-text-property hi-beg hi-end 'xref-match t substring) - (list substring (1+ (current-line)) (eglot-current-column) - (- end beg)))))) + (list substring (line-number-at-pos (point) t) + (eglot-current-column) (- end beg)))))) (`(,summary ,line ,column ,length) (cond (visiting (with-current-buffer visiting (funcall collect))) From 5b902b5cbb1de5f3852856d194deaf996c2623cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 26 Sep 2022 11:43:39 +0100 Subject: [PATCH 719/771] Add support for "single server, multiple modes" Previously, if an entry such as: ((c++-mode c-mode) . ("clangd)") were found in eglot-server-programs, it meant that opening a .cpp file and a .c file in the same project and enabling eglot for both would lead to two clangd instances. Now only one instance is created to handle all buffers of those major modes, as long as they are in the same project. This change accomplishes this with minimal changes and NO modification to the already complicated syntax of eglot-server-programs. Naturally, this means that a subtle backward-incompatibility was introduced. If, instead of "clangd", someone is using some kind "c++-or-c-but-not-both-at-once" server, this commit now breaks that person's configuration. After analysing the entries of this variable, an educated guess was made that this situation is rare. If it's not rare, then some change to the syntax of eglot-server-programs will have to ensue. * eglot.el (eglot-server-programs): Update docstring. (eglot-lsp-server): Replace major-mode -> major-modes. (eglot--lookup-mode): Rework. (eglot--guess-contact): Rework. (eglot--connect): Reword first parameter. (eglot-reconnect): Use eglot--major-modes. (eglot--read-server): Rework. (eglot--ensure-list): New helper. (eglot-current-server): Rework. (eglot-handle-request workspace/configuration): Use first of managed major modes. * NEWS.md: Mention change. GitHub-reference: per https://github.com/joaotavora/eglot/issues/681 --- lisp/progmodes/eglot.el | 70 ++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index fa29c606179..6a1eb1282c7 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -218,7 +218,8 @@ MAJOR-MODE can be: * A list combining the previous two alternatives, meaning multiple major modes will be associated with a single server - program. + program. This association is such that the same resulting + server process will manage buffers of different major modes. CONTACT can be: @@ -760,9 +761,9 @@ treated as in `eglot-dbind'." :documentation "Short nickname for the associated project." :accessor eglot--project-nickname :reader eglot-project-nickname) - (major-mode - :documentation "Major mode symbol." - :accessor eglot--major-mode) + (major-modes + :documentation "Major modes server is responsible for in a given project." + :accessor eglot--major-modes) (language-id :documentation "Language ID string for the mode." :accessor eglot--language-id) @@ -879,16 +880,31 @@ PRESERVE-BUFFERS as in `eglot-shutdown', which see." (defun eglot--lookup-mode (mode) "Lookup `eglot-server-programs' for MODE. -Return (LANGUAGE-ID . CONTACT-PROXY). If not specified, -LANGUAGE-ID is determined from MODE." +Return (MANAGED-MODES LANGUAGE-ID CONTACT-PROXY). + +MANAGED-MODES is a list with MODE as its first elements. +Subsequent elements are other major modes also potentially +managed by the server that is to manage MODE. + +If not specified in `eglot-server-programs' (which see), +LANGUAGE-ID is determined from MODE's name. + +CONTACT-PROXY is the value of the corresponding +`eglot-server-programs' entry." (cl-loop for (modes . contact) in eglot-server-programs + for mode-symbols = (cons mode + (delete mode + (mapcar #'car + (mapcar #'eglot--ensure-list + (eglot--ensure-list modes))))) thereis (cl-some (lambda (spec) (cl-destructuring-bind (probe &key language-id &allow-other-keys) - (if (consp spec) spec (list spec)) + (eglot--ensure-list spec) (and (provided-mode-derived-p mode probe) - (cons + (list + mode-symbols (or language-id (or (get mode 'eglot-language-id) (get spec 'eglot-language-id) @@ -903,7 +919,7 @@ Return (MANAGED-MODE PROJECT CLASS CONTACT LANG-ID). If INTERACTIVE is non-nil, maybe prompt user, else error as soon as something can't be guessed." (let* ((guessed-mode (if buffer-file-name major-mode)) - (managed-mode + (main-mode (cond ((and interactive (or (>= (prefix-numeric-value current-prefix-arg) 16) @@ -916,10 +932,11 @@ be guessed." ((not guessed-mode) (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) - (lang-id-and-guess (eglot--lookup-mode guessed-mode)) - (language-id (or (car lang-id-and-guess) + (triplet (eglot--lookup-mode main-mode)) + (managed-modes (car triplet)) + (language-id (or (cadr triplet) (string-remove-suffix "-mode" (symbol-name guessed-mode)))) - (guess (cdr lang-id-and-guess)) + (guess (caddr triplet)) (guess (if (functionp guess) (funcall guess interactive) guess)) @@ -945,7 +962,7 @@ be guessed." (cond (current-prefix-arg base-prompt) ((null guess) (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" - managed-mode base-prompt)) + main-mode base-prompt)) ((and program (not (file-name-absolute-p program)) (not (eglot--executable-find program t))) @@ -967,7 +984,7 @@ be guessed." full-program-invocation 'eglot-command-history))) guess))) - (list managed-mode (eglot--current-project) class contact language-id))) + (list managed-modes (eglot--current-project) class contact language-id))) (defvar eglot-lsp-context) (put 'eglot-lsp-context 'variable-documentation @@ -1038,7 +1055,7 @@ INTERACTIVE is t if called interactively." (interactive (list (eglot--current-server-or-lose) t)) (when (jsonrpc-running-p server) (ignore-errors (eglot-shutdown server interactive nil 'preserve-buffers))) - (eglot--connect (eglot--major-mode server) + (eglot--connect (eglot--major-modes server) (eglot--project server) (eieio-object-class-name server) (eglot--saved-initargs server) @@ -1115,12 +1132,12 @@ Each function is passed the server as an argument") (defvar-local eglot--cached-server nil "A cached reference to the current EGLOT server.") -(defun eglot--connect (managed-major-mode project class contact language-id) - "Connect to MANAGED-MAJOR-MODE, LANGUAGE-ID, PROJECT, CLASS and CONTACT. +(defun eglot--connect (managed-modes project class contact language-id) + "Connect to MANAGED-MODES, LANGUAGE-ID, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." (let* ((default-directory (project-root project)) (nickname (file-name-base (directory-file-name default-directory))) - (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) + (readable-name (format "EGLOT (%s/%s)" nickname managed-modes)) autostart-inferior-process server-info (contact (if (functionp contact) (funcall contact) contact)) @@ -1180,7 +1197,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) - (setf (eglot--major-mode server) managed-major-mode) + (setf (eglot--major-modes server) (eglot--ensure-list managed-modes)) (setf (eglot--language-id server) language-id) (setf (eglot--inferior-process server) autostart-inferior-process) (run-hook-with-args 'eglot-server-initialized-hook server) @@ -1236,7 +1253,7 @@ This docstring appeases checkdoc, that's all." (setf (eglot--inhibit-autoreconnect server) (null eglot-autoreconnect))))))) (let ((default-directory (project-root project)) - (major-mode managed-major-mode)) + (major-mode (car managed-modes))) (hack-dir-local-variables-non-file-buffer) (run-hook-with-args 'eglot-connect-hook server)) (eglot--message @@ -1244,7 +1261,7 @@ This docstring appeases checkdoc, that's all." in project `%s'." (or (plist-get serverInfo :name) (jsonrpc-name server)) - managed-major-mode + managed-modes (eglot-project-nickname server)) (when tag (throw tag t)))) :timeout eglot-connect-timeout @@ -1545,8 +1562,8 @@ and just return it. PROMPT shouldn't end with a question mark." being hash-values of eglot--servers-by-project append servers)) (name (lambda (srv) - (format "%s/%s" (eglot-project-nickname srv) - (eglot--major-mode srv))))) + (format "%s %s" (eglot-project-nickname srv) + (eglot--major-modes srv))))) (cond ((null servers) (eglot--error "No servers!")) ((or (cdr servers) (not dont-if-just-the-one)) @@ -1570,6 +1587,8 @@ and just return it. PROMPT shouldn't end with a question mark." (defun eglot--plist-keys (plist) "Get keys of a plist." (cl-loop for (k _v) on plist by #'cddr collect k)) +(defun eglot--ensure-list (x) (if (listp x) x (list x))) + ;;; Minor modes ;;; @@ -1700,7 +1719,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (or eglot--cached-server (cl-find major-mode (gethash (eglot--current-project) eglot--servers-by-project) - :key #'eglot--major-mode) + :key #'eglot--major-modes + :test #'memq) (and eglot-extend-to-xref buffer-file-name (gethash (expand-file-name buffer-file-name) @@ -2288,7 +2308,7 @@ When called interactively, use the currently active server" (file-directory-p uri-path)) (file-name-as-directory uri-path) (project-root (eglot--project server))))) - (setq-local major-mode (eglot--major-mode server)) + (setq-local major-mode (car (eglot--major-modes server))) (hack-dir-local-variables-non-file-buffer) (cl-loop for (wsection o) on (eglot--workspace-configuration-plist server) From 1780b93d664e6f4b4678d240333d7011e1a9f1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 26 Sep 2022 13:35:34 +0100 Subject: [PATCH 720/771] Make clojure-lsp handle more major modes at once Suggested-by: Witoslaw Koczewski * eglot.el (eglot-server-programs): Enhance clojure-specific section. GitHub-reference: per https://github.com/joaotavora/eglot/issues/682 --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 6a1eb1282c7..58a150cf48b 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -197,7 +197,8 @@ language-server/bin/php-language-server.php")) (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) (dockerfile-mode . ("docker-langserver" "--stdio")) - (clojure-mode . ("clojure-lsp")) + ((clojure-mode clojurescript-mode clojurec-mode) + . ("clojure-lsp")) (csharp-mode . ("omnisharp" "-lsp")) (purescript-mode . ("purescript-language-server" "--stdio")) (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run")) From b633c29648d0f9b4f36c81c6c0eac4c6e2362a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 4 Oct 2022 19:32:02 +0100 Subject: [PATCH 721/771] Rename "eglot -> eglot" in docstrings * eglot.el (eglot-mode-line, eglot-client-capabilities) (eglot--stay-out-of-p, eglot-managed-p) (eglot-managed-mode-hook, eglot--managed-mode) (eglot-current-server, eglot--current-server-or-lose) (eglot--mode-line-format, eglot-xref-backend) (eglot-imenu): Rename "EGLOT" -> "Eglot" --- lisp/progmodes/eglot.el | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 58a150cf48b..010f8c86c70 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -273,7 +273,7 @@ CONTACT can be: (defface eglot-mode-line '((t (:inherit font-lock-constant-face :weight bold))) - "Face for package-name in EGLOT's mode line.") + "Face for package-name in Eglot's mode line.") (defface eglot-diagnostic-tag-unnecessary-face '((t (:inherit shadow))) @@ -671,7 +671,7 @@ treated as in `eglot-dbind'." method))) (cl-defgeneric eglot-client-capabilities (server) - "What the EGLOT LSP client supports for SERVER." + "What the Eglot LSP client supports for SERVER." (:method (s) (list :workspace (list @@ -1131,7 +1131,7 @@ Each function is passed the server as an argument") contact)) (defvar-local eglot--cached-server nil - "A cached reference to the current EGLOT server.") + "A cached reference to the current Eglot server.") (defun eglot--connect (managed-modes project class contact language-id) "Connect to MANAGED-MODES, LANGUAGE-ID, PROJECT, CLASS and CONTACT. @@ -1625,7 +1625,7 @@ For example, to keep your Company customization, add the symbol `company' to this variable.") (defun eglot--stay-out-of-p (symbol) - "Tell if EGLOT should stay of of SYMBOL." + "Tell if Eglot should stay of of SYMBOL." (cl-find (symbol-name symbol) eglot-stay-out-of :test (lambda (s thing) (let ((re (if (symbolp thing) (symbol-name thing) thing))) @@ -1637,15 +1637,15 @@ For example, to keep your Company customization, add the symbol (setq-local ,symbol ,binding))) (defun eglot-managed-p () - "Tell if current buffer is managed by EGLOT." + "Tell if current buffer is managed by Eglot." eglot--managed-mode) (defvar eglot-managed-mode-hook nil - "A hook run by EGLOT after it started/stopped managing a buffer. + "A hook run by Eglot after it started/stopped managing a buffer. Use `eglot-managed-p' to determine if current buffer is managed.") (define-minor-mode eglot--managed-mode - "Mode for source buffers managed by some EGLOT project." + "Mode for source buffers managed by some Eglot project." :init-value nil :lighter nil :keymap eglot-mode-map (cond (eglot--managed-mode @@ -1715,7 +1715,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (eglot--managed-mode -1)) (defun eglot-current-server () - "Return logical EGLOT server for current buffer, nil if none." + "Return logical Eglot server for current buffer, nil if none." (setq eglot--cached-server (or eglot--cached-server (cl-find major-mode @@ -1728,7 +1728,7 @@ Use `eglot-managed-p' to determine if current buffer is managed.") eglot--servers-by-xrefed-file))))) (defun eglot--current-server-or-lose () - "Return current logical EGLOT server connection or error." + "Return current logical Eglot server connection or error." (or (eglot-current-server) (jsonrpc-error "No current JSON-RPC connection"))) @@ -1859,7 +1859,7 @@ Uses THING, FACE, DEFS and PREPEND." mouse-face mode-line-highlight)))) (defun eglot--mode-line-format () - "Compose the EGLOT's mode-line." + "Compose the Eglot's mode-line." (pcase-let* ((server (eglot-current-server)) (nick (and server (eglot-project-nickname server))) (pending (and server (hash-table-count @@ -2416,7 +2416,7 @@ may be called multiple times (respecting the protocol of :region (cons (point-min) (point-max)))) (setq eglot--diagnostics diags)) -(defun eglot-xref-backend () "EGLOT xref backend." 'eglot) +(defun eglot-xref-backend () "Eglot xref backend." 'eglot) (defvar eglot--temp-location-buffers (make-hash-table :test #'equal) "Helper variable for `eglot--handling-xrefs'.") @@ -2666,7 +2666,7 @@ for which LSP on-type-formatting should be requested." :deferred method)))) (defun eglot-completion-at-point () - "EGLOT's `completion-at-point' function." + "Eglot's `completion-at-point' function." ;; Commit logs for this function help understand what's going on. (when-let (completion-capability (eglot--server-capable :completionProvider)) (let* ((server (eglot--current-server-or-lose)) @@ -2983,7 +2983,7 @@ for which LSP on-type-formatting should be requested." nil))) (defun eglot-imenu () - "EGLOT's `imenu-create-index-function'. + "Eglot's `imenu-create-index-function'. Returns a list as described in docstring of `imenu--index-alist'." (cl-labels ((unfurl (obj) From b07fa37d04d98fcd9856f9f29fc1323850c230b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 4 Oct 2022 21:11:38 +0100 Subject: [PATCH 722/771] Add half-baked m-x eglot-list-connections Not very useful for now, but more functionality could be added later, like bindings for disconnecting a given connection, switching to its events buffers, or just listing some details like capabilities. * eglot.el (eglot-list-connections-mode, eglot-list-connections): New mode and function. --- lisp/progmodes/eglot.el | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 010f8c86c70..918bba62103 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3316,6 +3316,40 @@ If NOERROR, return predicate, else erroring function." (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) + +;;; List connections mode + +(define-derived-mode eglot-list-connections-mode tabulated-list-mode + "" "Eglot Connection List Mode + +\\{sly-connection-list-mode-map}" + (setq-local tabulated-list-format + `[("Language server" 16) ("Project name" 16) ("Modes handled" 16)]) + (tabulated-list-init-header)) + +(defun eglot-list-connections () + "List currently active Eglot connections." + (interactive) + (with-current-buffer + (get-buffer-create "*EGLOT connections*") + (let ((inhibit-read-only t)) + (erase-buffer) + (eglot-list-connections-mode) + (setq-local tabulated-list-entries + (mapcar + (lambda (server) + (list server + `[,(or (plist-get (eglot--server-info server) :name) + (jsonrpc-name server)) + ,(eglot-project-nickname server) + ,(mapconcat #'symbol-name + (eglot--major-modes server) + ", ")])) + (cl-reduce #'append + (hash-table-values eglot--servers-by-project)))) + (revert-buffer) + (pop-to-buffer (current-buffer))))) + ;;; Hacks ;;; From add2926de85a48e102cd11017f160f9f5ebe410d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 5 Oct 2022 20:53:26 +0100 Subject: [PATCH 723/771] Make eglot-code-actions usable non-interactively * eglot.el (eglot--read-execute-code-action): New helper. (eglot-code-actions): Use new helper. Offer non-interactive version. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1070 --- lisp/progmodes/eglot.el | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 918bba62103..0dff2b0d86a 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3124,8 +3124,9 @@ Returns a list as described in docstring of `imenu--index-alist'." (let ((boftap (bounds-of-thing-at-point 'sexp))) (list (car boftap) (cdr boftap))))) -(defun eglot-code-actions (beg &optional end action-kind) - "Offer to execute actions of ACTION-KIND between BEG and END. +(defun eglot-code-actions (beg &optional end action-kind interactive) + "Find LSP code actions of type ACTION-KIND between BEG and END. +Interactively, offer to execute them. If ACTION-KIND is nil, consider all kinds of actions. Interactively, default BEG and END to region's bounds else BEG is point and END is nil, which results in a request for code actions @@ -3135,8 +3136,10 @@ at point. With prefix argument, prompt for ACTION-KIND." ,(and current-prefix-arg (completing-read "[eglot] Action kind: " '("quickfix" "refactor.extract" "refactor.inline" - "refactor.rewrite" "source.organizeImports"))))) - (unless (eglot--server-capable :codeActionProvider) + "refactor.rewrite" "source.organizeImports"))) + t)) + (unless (or (not interactive) + (eglot--server-capable :codeActionProvider)) (eglot--error "Server can't execute code actions!")) (let* ((server (eglot--current-server-or-lose)) (actions @@ -3154,13 +3157,20 @@ at point. With prefix argument, prompt for ACTION-KIND." collect it)] ,@(when action-kind `(:only [,action-kind])))) :deferred t)) - (menu-items - (or (cl-loop for action across actions - ;; Do filtering ourselves, in case the `:only' - ;; didn't go through. - when (or (not action-kind) - (equal action-kind (plist-get action :kind))) - collect (cons (plist-get action :title) action)) + ;; Redo filtering, in case the `:only' didn't go through. + (actions (cl-loop for a across actions + when (or (not action-kind) + (equal action-kind (plist-get a :kind))) + collect a))) + (if interactive + (eglot--read-execute-code-action actions server action-kind) + actions))) + +(defun eglot--read-execute-code-action (actions server &optional action-kind) + "Helper for interactive calls to `eglot-code-actions'" + (let* ((menu-items + (or (cl-loop for a in actions + collect (cons (plist-get a :title) a)) (apply #'eglot--error (if action-kind `("No \"%s\" code actions here" ,action-kind) `("No code actions here"))))) @@ -3169,7 +3179,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (plist-get (cdr menu-item) :isPreferred)) menu-items)) (default-action (car (or preferred-action (car menu-items)))) - (action (if (and action-kind (null (cadr menu-items))) + (chosen (if (and action-kind (null (cadr menu-items))) (cdr (car menu-items)) (if (listp last-nonmenu-event) (x-popup-menu last-nonmenu-event `("Eglot code actions:" @@ -3179,7 +3189,7 @@ at point. With prefix argument, prompt for ACTION-KIND." default-action) menu-items nil t nil nil default-action) menu-items)))))) - (eglot--dcase action + (eglot--dcase chosen (((Command) command arguments) (eglot-execute-command server (intern command) arguments)) (((CodeAction) edit command) From 0848387fa23c2cf00e7fae7192b21cad91e5b921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 8 Oct 2022 11:19:10 +0100 Subject: [PATCH 724/771] Fix docstring of eglot-list-connections-mode * eglot.el (eglot-list-connections-mode): Fix mistaken reference to similar non-Eglot code. --- lisp/progmodes/eglot.el | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0dff2b0d86a..0cba701f513 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -3330,9 +3330,8 @@ If NOERROR, return predicate, else erroring function." ;;; List connections mode (define-derived-mode eglot-list-connections-mode tabulated-list-mode - "" "Eglot Connection List Mode - -\\{sly-connection-list-mode-map}" + "" "Eglot mode for listing server connections +\\{eglot-list-connections-mode-map}" (setq-local tabulated-list-format `[("Language server" 16) ("Project name" 16) ("Modes handled" 16)]) (tabulated-list-init-header)) From 4071eaf8ad9ce7e1a19f9f12c20430a77c1807c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 11 Oct 2022 12:02:46 +0100 Subject: [PATCH 725/771] * eglot.el (version): actually bump to 1.9 --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0cba701f513..d3f5935a9ef 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,7 +2,7 @@ ;; Copyright (C) 2018-2022 Free Software Foundation, Inc. -;; Version: 1.8 +;; Version: 1.9 ;; Author: João Távora ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot From efd3ef3ceb799fa26be8f84d54d4a21ebc4a5d31 Mon Sep 17 00:00:00 2001 From: Manuel Giraud Date: Sat, 15 Oct 2022 18:58:56 +0200 Subject: [PATCH 726/771] ; * src/window.c: Fix some comments. (Bug#58550) --- src/window.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/window.c b/src/window.c index ed30544ff83..51fd2abc5b5 100644 --- a/src/window.c +++ b/src/window.c @@ -5403,12 +5403,13 @@ window_wants_mode_line (struct window *w) * Return 1 if window W wants a header line and is high enough to * accommodate it, 0 otherwise. * - * W wants a header line if it's a leaf window and neither a minibuffer - * nor a pseudo window. Moreover, its 'window-mode-line-format' - * parameter must not be 'none' and either that parameter or W's - * buffer's 'mode-line-format' value must be non-nil. Finally, W must - * be higher than its frame's canonical character height and be able to - * accommodate a mode line too if necessary (the mode line prevails). + * W wants a header line if it's a leaf window and neither a + * minibuffer nor a pseudo window. Moreover, its + * 'window-header-line-format' parameter must not be 'none' and either + * that parameter or W's buffer's 'header-line-format' value must be + * non-nil. Finally, W must be higher than its frame's canonical + * character height and be able to accommodate a mode line too if + * necessary (the mode line prevails). */ bool window_wants_header_line (struct window *w) @@ -5436,9 +5437,9 @@ window_wants_header_line (struct window *w) * accommodate it, 0 otherwise. * * W wants a tab line if it's a leaf window and neither a minibuffer - * nor a pseudo window. Moreover, its 'window-mode-line-format' + * nor a pseudo window. Moreover, its 'window-tab-line-format' * parameter must not be 'none' and either that parameter or W's - * buffer's 'mode-line-format' value must be non-nil. Finally, W must + * buffer's 'tab-line-format' value must be non-nil. Finally, W must * be higher than its frame's canonical character height and be able * to accommodate a mode line and a header line too if necessary (the * mode line and a header line prevail). From 3e5856b21a83a52dda8c0b3ab541d106d809d625 Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Tue, 18 Oct 2022 12:17:51 +0100 Subject: [PATCH 727/771] Add new Texinfo manual for the Eglot LSP client * doc/misc/eglot.texi: New file. --- doc/misc/eglot.texi | 1116 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1116 insertions(+) create mode 100644 doc/misc/eglot.texi diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi new file mode 100644 index 00000000000..761964334bb --- /dev/null +++ b/doc/misc/eglot.texi @@ -0,0 +1,1116 @@ +\input texinfo @c -*-texinfo-*- +@c %**start of header +@setfilename ../../eglot.info +@settitle Eglot: The Emacs Client for Language Server Protocol +@include docstyle.texi +@syncodeindex vr cp +@syncodeindex fn cp +@c %**end of header + +@copying +This manual is for Eglot, the Emacs LSP client. + +Copyright @copyright{} 2022 Free Software Foundation, Inc. + +@quotation +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.3 or +any later version published by the Free Software Foundation; with no +Invariant Sections, with the Front-Cover Texts being ``A GNU Manual'', +and with the Back-Cover Texts as in (a) below. A copy of the license +is included in the section entitled ``GNU Free Documentation License''. + +(a) The FSF's Back-Cover Text is: ``You have the freedom to copy and +modify this GNU manual.'' +@end quotation +@end copying + +@dircategory Emacs misc features +@direntry +* Eglot: (eglot). Language Server Protocol client for Emacs. +@end direntry + +@titlepage +@sp 4 +@c The title is printed in a large font. +@center @titlefont{User's Guide} +@sp 1 +@center @titlefont{to} +@sp 1 +@center @titlefont{Eglot: The Emacs LSP Client} +@ignore +@sp 2 +@center release 1.8 +@c -release- +@end ignore +@sp 3 +@center Jo@~ao T@'avora & Eli Zaretskii +@c -date- + +@page +@vskip 0pt plus 1filll +@insertcopying +@end titlepage + +@contents + +@ifnottex +@node Top +@top Eglot + +@cindex LSP +@cindex language server protocol +Eglot is the Emacs client for Language Server Protocol. The name +``Eglot'' is an acronym that stands for ``@emph{E}macs +Poly@emph{glot}''.@footnote{ +A @dfn{polyglot} is a person who is able to use several languages. +} Eglot provides infrastructure and a set of commands for enriching +Emacs source code editing capabilities using the @dfn{Language Server +Protocol} (@acronym{LSP}) -- a standardized communications protocol +between source code editors (such as Emacs) and language servers -- +programs external to Emacs that analyze the source code on behalf of +Emacs. The protocol allows Emacs to receive various source code +services from the server, such as description and location of +functions calls, types of variables, class definitions, syntactic +errors, etc. This way, Emacs doesn't need to implement the +language-specific parsing and analysis capabilities in its own code, +but is still capable of providing sophisticated editing features that +rely on such capabilities, such as automatic code completion, go-to +definition of function/class, documentation of symbol at-point, +refactoring, on-the-fly diagnostics, and more. + +Eglot itself is completely language-agnostic, but it can support any +programming language for which there is a language server and an Emacs +major mode. + +This manual documents how to configure, use, and customize Eglot. + +@insertcopying + +@menu +* Quick Start:: For the impatient. +* Eglot and LSP Servers:: How to work with language servers +* Using Eglot:: Important Eglot commands and variables. +* Customizing Eglot:: Eglot customization and advanced features. +* Troubleshooting Eglot:: Troubleshooting and reporting bugs. +* GNU Free Documentation License:: The license for this manual. +* Index:: +@end menu +@end ifnottex + +@node Quick Start +@chapter Quick Start +@cindex quick start + +This chapter provides concise instructions for setting up and using +Eglot with your programming project in common usage scenarios. For +more detailed instructions regarding Eglot setup, @pxref{Eglot and LSP +Servers}. @xref{Using Eglot}, for detailed description of using Eglot, +and see @ref{Customizing Eglot}, for adapting Eglot to less common use +patterns. + +Here's how to start using Eglot with your programming project: + +@enumerate +@item +Select and install a language server. + +Eglot comes pre-configured with many popular language servers, see the +value of @code{eglot-server-programs}. If the server(s) mentioned +there satisfy your needs for the programming language(s) with which +you want to use Eglot, you just need to make sure those servers are +installed on your system. Alternatively, install one or more servers +of your choice and add them to the value of +@code{eglot-server-programs}, as described in @ref{Setting Up LSP +Server}. + +@item +Turn on Eglot for your project. + +To start using Eglot for a project, type @kbd{M-x eglot @key{RET}} in +a buffer visiting any file that belongs to the project. This starts +the language server configured for the programming language of that +buffer, and causes Eglot to start managing all the files of the +project which use the same programming language. The notion of a +``project'' used by Eglot is the same Emacs uses (@pxref{Projects,,, +emacs, GNU Emacs Manual}): in the simplest case, the ``project'' is +the single file you are editing, but it can also be all the files in a +single directory or a directory tree under some version control +system, such as Git. + +Alternatively, you can start Eglot automatically from the major-mode +hook of the mode used for the programming language; see @ref{Starting +Eglot}. + +@item +Use Eglot. + +Most Eglot facilities are integrated into Emacs features, such as +ElDoc, Flymake, Xref, and Imenu. However, Eglot also provides +commands of its own, mainly to perform tasks by the LSP server, such +as @kbd{M-x eglot-rename} (to rename an identifier across the entire +project), @kbd{M-x eglot-format} (to reformat and reindent code), and +some others. @xref{Eglot Commands}, for the detailed list of Eglot +commands. + +@item +That's it! +@end enumerate + +@node Eglot and LSP Servers +@chapter Eglot and LSP Servers + +This chapter describes how to set up Eglot for your needs, and how to +start it. + +@menu +* Setting Up LSP Server:: How to configure LSP servers for your needs. +* Starting Eglot:: Ways of starting Eglot for your project. +* Shutting Down LSP Server:: +@end menu + +@node Setting Up LSP Server +@section Setting Up LSP Server +@cindex setting up LSP server for Eglot +@cindex language server for Eglot + +For Eglot to be useful, it must first be combined with a suitable +language server. Usually, that means running the server program +locally as a child process of Emacs (@pxref{Processes,,, elisp, GNU +Emacs Lisp Reference Manual}) and communicating with it via the +standard input and output streams. + +The language server program must be installed separately, and is not +further discussed in this manual; refer to the documentation of the +particular server(s) you want to install. + +To use a language server, Eglot must know how to start it and which +programming languages each server supports. This information is +provided by the @code{eglot-server-programs} variable. + +@defvar eglot-server-programs +This variable associates major modes with names and command-line +arguments of the language server programs corresponding to the +programming language of each major mode. This variable provides all +the information that Eglot needs to know about the programming +language of the source you are editing. + +The value of the variable is an alist, whose elements are of the form +@w{@code{(@var{major-mode} . @var{server})}}. + +The @var{major-mode} of the alist elements can be either a symbol of +an Emacs major mode or a list of the form @w{@code{(@var{mode} +:language-id @var{id})}}, with @var{mode} being a major-mode symbol +and @var{id} a string that identifies the language to the server (if +Eglot cannot by itself convert the major-mode to the language +identifier string required by the server). In addition, +@var{major-mode} can be a list of several major mode specified in one +of the above forms -- this means the server can support more than one +major mode. + +The @var{server} part of the alist elements can be one of the +following: + +@table @code +@item (@var{program} @var{args}@dots{}) +This says to invoke @var{program} with zero or more arguments +@var{args}; the program is expected to communicate with Emacs via the +standard input and standard output streams. + +@item (@var{program} @var{args}@dots{} :initializationOptions @var{options}@dots{}) +Like above, but with @var{options} specifying the options to be +used for constructing the @samp{initializationOptions} JSON object for +the server. @var{options} can also be a function of one argument, in +which case it will be called with the server instance as the argument, +and should return the JSON object to use for initialization. + +@item (@var{host} @var{port} @var{args}@dots{}) +Here @var{host} is a string and @var{port} is a positive integer +specifying a TCP connection to a remote server. The @var{args} are +passed to @code{open-network-stream}, e.g.@: if the connection needs +to use encryption or other non-default parameters (@pxref{Network,,, +elisp, GNU Emacs Lisp Reference Manual}). + +@item (@var{program} @var{args}@dots{} :autoport @var{moreargs}@dots{}) +@var{program} is started with a command line constructed from +@var{args} followed by an available server port and the rest of +arguments in @var{moreargs}; Eglot then establishes a TCP connection +with the server via that port on the local host. + +@item @var{function} +This should be a function of a single argument: non-@code{nil} if the +connection was requested interactively (e.g., by the @code{eglot} +command), otherwise @code{nil}. The function should return a value of +any of the forms described above. This allows interaction with the +user for determining the program to start and its command-line +arguments. +@end table + +@end defvar + +Eglot comes with a fairly complete set of associations of major-modes +to popular language servers predefined in this variable. If you need +to add server associations to the default list, use +@code{add-to-list}. For example, if there is a hypothetical language +server program @command{fools} for the language @code{Foo} which is +supported by an Emacs major-mode @code{foo-mode}, you can add it to +the alist like this: + +@lisp +(add-to-list 'eglot-server-programs + '(foo-mode . ("fools" "--stdio"))) +@end lisp + +This will invoke the program @command{fools} with the command-line +argument @option{--stdio} in support of editing source files for which +Emacs turns on @code{foo-mode}, and will communicate with the program +via the standard streams. As usual with invoking programs, the +executable file @file{fools} should be in one of the directories +mentioned by the @code{exec-path} variable (@pxref{Subprocess +Creation,,, elisp, GNU Emacs Lisp Reference Manual}), for Eglot to be +able to find it. + +@node Starting Eglot +@section Starting Eglot +@cindex starting Eglot +@cindex activating Eglot for a project + +@findex eglot +The most common way to start Eglot is to simply visit a source file of +a given language and use the command @kbd{M-x eglot}. This starts the +language server suitable for the visited file's major-mode, and +attempts to connect to it. If the connection to the language server +is successful, you will see the @code{eglot:@var{server}} indicator on +the mode line which reflects the server that was started. If the +server program couldn't be started or connection to it failed, you +will see an error message; in that case, try to troubleshoot the +problem as described in @ref{Troubleshooting Eglot}. +@c FIXME: Is the mode-line indication just eglot:server, or +@c egloit:serve/project, as described farther down? + +Once a language server was successfully started and Eglot connected to +it, you can immediately start using the Emacs features supported by +Eglot, as described in @ref{Eglot Features}. + +A single Eglot session for a certain major-mode usually serves all the +buffers under that mode which visit files from the same project, so +you don't need to invoke @kbd{M-x eglot} again when you visit another +file from the same project which is edited using the same major-mode. +This is because Eglot uses the Emacs project infrastructure, as +described in @ref{Eglot and Buffers}, and this knows about files that +belong to the same project. Thus, after starting an Eglot session for +some buffer, that session is automatically reused when visiting files +in the same project with the same major-mode. + +@findex eglot-ensure +Alternatively, you could configure Eglot to start automatically for +one or more major-modes from the respective mode hooks. Here's an +example for a hypothetical @code{foo-mode}: + +@lisp + (add-hook 'foo-mode-hook 'eglot-ensure) +@end lisp + +@noindent +The function @code{eglot-ensure} will start an Eglot session for each +buffer in which @code{foo-mode} is turned on, if there isn't already +an Eglot session that handles the buffer. Note that this variant of +starting an Eglot session is non-interactive, so it should be used +only when you are confident that Eglot can be started reliably for any +file which may be visited with the major-mode in question. + +When Eglot connects to a language server for the first time in an +Emacs session, it runs the hook @code{eglot-connect-hook} +(@pxref{Eglot Variables}). + +@node Shutting Down LSP Server +@section Shutting Down LSP Server +@cindex shutting down LSP server + +When Eglot is turned on, it arranges for turning itself off +automatically if the language server process terminates. Turning off +Eglot means it shuts down the server connection, ceases its management +of all the buffers that use the server connection which was +terminated, deactivates its minor mode, and restores the original +values of the Emacs variables that Eglot changed when it was turned +on. @xref{Eglot and Buffers}, for more details of what does Eglot +management of a buffer entail. + +@findex eglot-shutdown +You can also shut down a language server manually, by using the +command @kbd{M-x eglot-shutdown}. This prompts for the server (unless +there's only one connection and it's used in the current buffer), and +then shuts it down. By default, it also kills the server's events +buffer (@pxref{Troubleshooting Eglot}), but a prefix argument prevents +that. + +Alternatively, you can customize the variable +@code{eglot-autoshutdown} to a non-@code{nil} value, in which case +Eglot will automatically shut down the language server process when +the last buffer served by that language server is killed. The default +of this variable is @code{nil}, so that visiting another file would +automatically activate Eglot even when the project which started Eglot +with the server no longer has any buffer associated with it. This +default allows you to start a server only once in each Emacs session. + +@node Using Eglot +@chapter Using Eglot + +This chapter describes in detail the features that Eglot provides and +how it does that. It also provides reference sections for Eglot +commands and variables. + +@menu +* Eglot Features:: +* Eglot and Buffers:: +* Eglot Commands:: +* Eglot Variables:: +@end menu + +@node Eglot Features +@section Eglot Features +@cindex features in buffers supported by Eglot + +Once Eglot is enabled in a buffer, it uses LSP and the language-server +capabilities to activate, enable, and enhance modern IDE features in +Emacs. The features themselves are usually provided via other Emacs +packages. Here are the main features Eglot enables and provides: + +@itemize @bullet +@item +At-point documentation: when point is at or near a symbol or an +identifier, the information about the symbol/identifier, such as the +signature of a function or class method and server-generated +diagnostics, is made available via the ElDoc package (@pxref{Lisp +Doc,,, emacs, GNU Emacs Manual}). This allows major modes to provide +extensive help and documentation about the program identifiers. + +@item +On-the-fly diagnostic annotations with server-suggested fixes, via the +Flymake package (@pxref{Top,,, flymake, GNU Flymake manual}). This +improves and enhances the Flymake diagnostics, replacing the other +Flymake backends. + +@item +Finding definitions and uses of identifiers, via Xref (@pxref{Xref,,, +emacs, GNU Emacs Manual}). Eglot provides a backend for the Xref +capabilities which uses the language-server understanding of the +program source. In particular, it eliminates the need to generate +tags tables (@pxref{Tags tables,,, emacs, GNU Emacs Manual}) for +languages which are only supported by the @code{etags} backend. + +@item +Buffer navigation by name of function, class, method, etc., via Imenu +(@pxref{Imenu,,, emacs, GNU Emacs Manual}). Eglot provides its own +variant of @code{imenu-create-index-function}, which generates the +index for the buffer based on language-server program source analysis. + +@item +Enhanced completion of symbol at point by the +@code{completion-at-point} command (@pxref{Symbol Completion,,, emacs, +GNU Emacs Manual}). This uses the language-server's parser data for +the completion candidates. + +@item +Automatic reformatting of source code as you type it. This is similar +to what the @code{eglot-format} command does (see below), but is +activated automatically as you type. + +@item +If @code{company-mode}, a popular 3rd-party completion package, is +installed, Eglot enhances it by providing completion candidates based +on the language-server analysis of the source code. + +@item +If @code{yasnippet}, a popular package for automatic insertion of code +templates, is installed, and the language server supports +template-based completion, Eglot provides to @code{yasnippet} +completion templates based on the language-server analysis and +knowledge bases. + +@item +In addition to enabling and enhancing other features and packages, +Eglot also provides a small number of user commands based directly on +the capabilities of language servers. These commands are: + +@table @kbd +@item M-x eglot-rename +This prompts for a new name for the symbol at point, and then modifies +all the project source files to rename the symbol to the new name, +based on editing data received from the language-server. @xref{Eglot +and Buffers}, for the details of how project files are defined. + +@item M-x eglot-format +This reformats and prettifies the current active region according to +source formatting rules of the language-server. If the region is not +active, it reformats the entire buffer instead. + +@item M-x eglot-format-buffer +This reformats and prettifies the current buffer according to source +formatting rules of the language-server. + +@cindex code actions +@item M-x eglot-code-actions +@itemx M-x eglot-code-action-organize-imports +@itemx M-x eglot-code-action-quickfix +@itemx M-x eglot-code-action-extract +@itemx M-x eglot-code-action-inline +@itemx M-x eglot-code-action-rewrite +These command allow you to invoke the so-called @dfn{code actions}: +requests for the language-server to provide editing commands for +various code fixes, typically either to fix an error diagnostic or to +beautify/refactor code. For example, +@code{eglot-code-action-organize-imports} rearranges the program +@dfn{imports}---declarations of modules whose capabilities the program +uses. These commands affect all the files that belong to the +project. The command @kbd{M-x eglot-code-actions} will pop up a menu +of code applicable actions at point. +@end table + +@end itemize + +Not all servers support the full set of LSP capabilities, but most of +them support enough to enable the basic set of features mentioned +above. Conversely, some servers offer capabilities for which no +equivalent Emacs package exists yet, and so Eglot cannot (yet) expose +these capabilities to Emacs users. + +@node Eglot and Buffers +@section Buffers, Projects, and Eglot +@cindex buffers managed by Eglot +@cindex projects and Eglot + +@cindex workspace +One of the main strong points of using a language server is that a +language server has a broad view of the program: it considers more +than just the single source file you are editing. Ideally, the +language server should know about all the source files of your program +which are written in the language supported by the server. In the +language-server parlance, the set of the source files of a program is +known as a @dfn{workspace}. The Emacs equivalent of a workspace is a +@dfn{project} (@pxref{Projects,,, emacs, GNU Emacs Manual}). Eglot +fully supports Emacs projects, and considers the file in whose buffer +Eglot is turned on as belonging to a project. In the simplest case, +that file is the entire project, i.e.@: your project consists of a +single file. But there are other more complex projects: + +@itemize @bullet +@item +A single-directory project: several source files in a single common +directory. + +@item +A VC project: source files in a directory hierarchy under some VCS, +i.e.@: a VCS repository (@pxref{Version Control,,, emacs, GNU Emacs +Manual}). + +@item +An EDE project: source files in a directory hierarchy managed via the +Emacs Development Environment (@pxref{EDE,,, emacs, GNU Emacs +Manual}). +@end itemize + +Eglot uses the Emacs's project management infrastructure to figure out +which files and buffers belong to what project, so any kind of project +supported by that infrastructure is automatically supported by Eglot. + +When Eglot starts a server program, it does so in the project's root +directory, which is usually the top-level directory of the project's +directory hierarchy. This ensures the language server has the same +comprehensive view of the project's files as you do. + +For example, if you visit the file @file{~/projects/fooey/lib/x.foo} +and @file{x.foo} belongs to a project rooted at +@file{~/projects/fooey} (perhaps because a @file{.git} directory +exists there), then @kbd{M-x eglot} causes the server program to start +with that root as the current working directory. The server then will +analyze not only the file @file{lib/x.foo} you visited, but likely +also all the other @file{*.foo} files under the +@file{~/projects/fooey} directory. + +In some cases, additional information specific to a given project will +need to be provided to the language server when starting it. The +variable @code{eglot-workspace-configuration} (@pxref{Customizing +Eglot}) exists for that purpose. It specifies the parameters and +their values to communicate to each language server which needs that. + +When Eglot is active for a project, it performs several background +activities on behalf of the project and its buffers: + +@itemize @bullet +@cindex mode-line indication of language server +@cindex mouse clicks on mode-line, and Eglot +@vindex eglot-menu +@item +All of the project's file-visiting buffers under the same major-mode +are served by a single language-server connection. (If the project +uses several programming languages, there will usually be a separate +server connection for each group of files written in the same language +and using the same Emacs major-mode.) Eglot adds the +@samp{eglot:@var{server}/@var{project}} indication to the mode line of +each such buffer, where @var{server} is the name of the server and +@var{project} identifies the project by its root directory. Clicking +the mouse on the Eglot mode-line indication activates a menu with +server-specific items. + +@item +For each buffer in which Eglot is active, it notifies the language +server that Eglot is @dfn{managing} the file visited by that buffer. +This tells the language server that the file's contents on disk may no +longer be up-to-date due to unsaved edits. Eglot reports to the +server any changes in the text of each managed buffer, to make the +server aware of unsaved changes. This includes your editing of the +buffer and also changes done automatically by other Emacs features and +commands. Killing a buffer relinquishes its management by Eglot and +notifies the server that the file on disk is up-to-date. + +@vindex eglot-managed-mode-hook +@vindex eglot-managed-p +@item +Eglot turns on a special minor mode in each buffer it manages. This +minor mode ensures the server is notified about files Eglot manages, +and also arranges for other Emacs features supported by Eglot +(@pxref{Eglot Features}) to receive information from the language +server, by changing the settings of these features. Unlike other +minor-modes, this special minor mode is not activated manually by the +user, but automatically as result of starting an Eglot session for the +buffer. However, this minor mode provides a hook variable +@code{eglot-managed-mode-hook} that can be used to customize the Eglot +management of the buffer. This hook is run both when the minor mode +is turned on and when it's turned off; use the variable +@code{eglot-managed-p} to tell if current buffer is still being +managed or not. When Eglot stops managing the buffer, this minor mode +is turned off, and all the settings that Eglot changed are restored to +their original values. + +@item +When you visit a file under the same project, whether an existing or a +new file, its buffer is automatically added to the set of buffers +managed by Eglot, and the server which supports the buffer's +major-mode is notified about that. Thus, visiting a non-existent file +@file{/home/joe/projects/fooey/lib/y.foo} in the above example will +notify the server of the @file{*.foo} files' language that a new file +was added to the project, even before the file appears on disk. The +special Eglot minor mode is also turned on automatically in the buffer +visiting the file. +@end itemize + +@node Eglot Commands +@section Eglot Commands +@cindex commands, Eglot + +This section provides a reference of the most commonly used Eglot +commands: + +@ftable @code +@item M-x eglot +This command adds the current buffer and the file it visits to the +group of buffers and files managed by Eglot on behalf of a suitable +language server. If a language server for the buffer's +@code{major-mode} (@pxref{Major Modes,,, emacs, GNU Emacs Manual}) is +not yet running, it will be started; otherwise the buffer and its file +will be added to those managed by an existing server session. + +The command attempts to figure out the buffer's major mode and the +suitable language server; in case it fails, it might prompt for the +major mode to use and for the server program to start. If invoked +with @kbd{C-u}, it always prompts for the server program, and if +invoked with @kbd{C-u C-u}, also prompt for the major mode. + +If the language server is successfully started and contacted, this +command arranges for any other buffers belonging to the same project +and using the same major mode to use the same language-server session. +That includes any buffers created by visiting files after this command +succeeds to connect to a language server. + +All the Emacs features that are capable of using Eglot services +(@pxref{Eglot Features}) are automatically configured by this command +to start using the language server via Eglot. To customize which +Emacs features will be configured to use Eglot, use the +@code{eglot-stay-out-of} option (@pxref{Customizing Eglot}). + +@item M-x eglot-reconnect +@c FIXME: When and why would this command be useful? Move to less common? +Reconnect to current language server. + +@item M-x eglot-shutdown +Shut down a language server. This commands prompts for a language +server to shut down (unless there's only one server session, and it +manages the current buffer). Then the command shuts down the server +and stops managing the buffers the server was used for. Emacs +features (@pxref{Eglot Features}) that Eglot configured to work with +the language server are restored back to their original configuration. + +Normally, this command kills the buffers used for communicating with +the language server, but if invoked with a prefix argument @kbd{C-u}, +the command doesn't kill those buffers, allowing them to be used for +diagnostics and problem reporting (@pxref{Troubleshooting Eglot}). + +@item M-x eglot-shutdown-all +This command shuts down all the language servers active in the current +Emacs session. As with @code{eglot-shutdown}, invoking this command +with a prefix argument avoids killing the buffers used for +communications with the language servers. + +@item M-x eglot-rename +This command renames the program symbol (a.k.a.@: @dfn{identifier}) at +point to another name. It prompts for the new name of the symbol, and +then modifies all the files in the project which arte managed by the +language server of the current buffer to implement the renaming. + +@item M-x eglot-format +This command reformats the active region according to the +language-server rules. If no region is active, it reformats the +entire current buffer. + +@item M-x eglot-format-buffer +This command reformats the current buffer, in the same manner as +@code{eglot-format} does. + +@item M-x eglot-code-actions +@itemx mouse-1 +This command asks the server for any @dfn{code actions} applicable at +point. It can also be invoked by @kbd{mouse-1} clicking on +diagnostics provided by the server. + +@item M-x eglot-code-action-organize-imports +@itemx M-x eglot-code-action-quickfix +@itemx M-x eglot-code-action-extract +@itemx M-x eglot-code-action-inline +@itemx M-x eglot-code-action-rewrite +These commands invoke specific code actions supported by the language +server. +@c FIXME: Need more detailed description of each action. +@end ftable + +The following Eglot commands are used less commonly, mostly for +diagnostic and troubleshooting purposes: + +@ftable @code +@item M-x eglot-events-buffer +This command pops up the events buffer used for communication with the +language server of the current buffer. + +@item M-x eglot-stderr-buffer +This command pops up the buffer with the debug info printed by the +language server to its standard error stream. + +@item M-x eglot-forget-pending-continuations +Forget pending requests for the server of the current buffer. +@c FIXME: Better description of the need. + +@item M-x eglot-signal-didChangeConfiguration +This command updates the language server configuration according to +the current value of the variable @code{eglot-workspace-configuration} +(@pxref{Customizing Eglot}). + +@item M-x eglot-clear-status +Clear the last JSONRPC error for the server of the current buffer. +@c FIXME: Better description of the need and the effect. +@end ftable + +As described in @ref{Eglot Features} most features associated with +Eglot are actually provided by other Emacs packages and features, and +Eglot only enhances them by allowing them to use the information +coming from the language servers. For completeness, here's the list +of commands of those other packages that are very commonly used in +Eglot-managed buffers: + +@c Not @ftable, because the index entries should mention Eglot +@table @code +@cindex eldoc, and Eglot +@cindex documentation using Eglot +@item M-x eldoc +Ask the ElDoc system for help at point. + +@cindex flymake, and Eglot +@cindex on-the-fly diagnostics using Eglot +@item M-x flymake-show-buffer-diagnostics +Ask Flymake system to display diagnostics for the current buffer. + +@item M-x flymake-show-project-diagnostics +Ask Flymake to list diagnostics for all the files in the current +project. + +@cindex xref, and Eglot +@cindex finding definitions of identifiers using Eglot +@item M-x xref-find-definitions +Ask Xref to go the definition of the identifier at point. + +@cindex imenu navigation using Eglot +@item M-x imenu +Let the user navigate the program source code using buffer index, +categorizing program elements by syntactic class (class, method, +variable, etc.) and offering completion. + +@cindex symbol completion using Eglot +@item M-x completion-at-point +Request completion of the symbol at point. +@end table + +@node Eglot Variables +@section Eglot Variables +@cindex variables, Eglot + +This section provides a reference of the Eglot' user options. + +@vtable @code +@item eglot-autoreconnect +This option controls the ability to reconnect automatically to the +language server. The default value 3 means to attempt reconnection +only if the previous successful connection lasted for more than that +number of seconds; a different positive value changes the minimal +length of the connection to trigger reconnection. A value of @code{t} +means always reconnect automatically, and @code{nil} means never +reconnect. The alternative to automatic reconnection is the command +@code{eglot-reconnect} (@pxref{Eglot Commands}). + +@item eglot-connect-timeout +This specifies the number of seconds before connection attempt to a +language server times out. The value of @code{nil} means never time +out. The default is 30 seconds. + +@item eglot-sync-connect +This controls whether attempts to connect to language servers should +be blocking. The setting is mainly important for slow connections. +The default value is 3; a positive value means block for that many +seconds, then wait for the connection in the background. The value +of @code{t} means block for @code{eglot-connect-timeout} seconds. The +value of @code{nil} or zero means don't block at all. +@c FIXME: the code doesn't use eglot-connect-timeout, it uses a +@c hard-coded value of 30. + +@item eglot-events-buffer-size +This determines the size of the Eglot events buffer. @xref{Eglot +Commands, eglot-events-buffer}, for how to display that buffer. If +the value is changed, for it to take effect the connection should be +restarted using @kbd{eglot-shutdown} followed by +@kbd{eglot-reconnect}. +@c FIXME: Shouldn't the defcustom do this by itself using the :set +@c attribute? +@xref{Troubleshooting Eglot}, for when this could be useful. + +@item eglot-autoshutdown +If this is non-@code{nil}, Eglot shuts down a language server when the +last buffer managed by it is killed. @xref{Shutting Down LSP Server}. +The default is @code{nil}; if you want to shut down a server, use +@kbd{M-x eglot-shutdown} (@pxref{Eglot Commands}). + +@item eglot-confirm-server-initiated-edits +Various Eglot commands and code actions result in the language server +sending editing commands to Emacs. If this option's value is +non-@code{nil} (the default), Eglot will ask for confirmation before +performing the edits requested by the server. +@c FIXME: Not clear: is the confirmation required for each individual +@c edit, or for as group? for each buffer or just once? And what +@c about on-type reformatting -- does that require confirmation as +@c well (which would be annoying)? + +@item eglot-ignored-server-capabilities +This variable's value is a list of language server capabilities that +Eglot should not use. The default is @code{nil}: Eglot uses all of +the capabilities supported by each server. + +@item eglot-extend-to-xref +If this is non-@code{nil}, and @kbd{M-.} +(@code{xref-find-definitions}) lands you in a file outside of your +project, such as a system-installed library or header file, +transiently consider that file as managed by the same language server. +That file is still outside your project (i.e. @code{project-find-file} +won't find it), but Eglot and the server will consider it to be part +of the workspace. The default is @code{nil}. + +@item eglot-mode-map +This variable is the keymap for binding Eglot-related command. It is +in effect only as long as the buffer is managed by Eglot. By default, +it is empty, with the single exception: @kbd{C-h .} is remapped to +invoke @code{eldoc-doc-buffer}. You can bind additional commands in +this map. For example: + +@lisp + (define-key eglot-mode-map (kbd "C-c r") 'eglot-rename) + (define-key eglot-mode-map (kbd "C-c o") 'eglot-code-action-organize-imports) + (define-key eglot-mode-map (kbd "C-c h") 'eldoc) + (define-key eglot-mode-map (kbd "") 'xref-find-definitions) +@end lisp + +@end vtable + +Additional variables, which are relevant for customizing the server +connections, are documented in @ref{Customizing Eglot}. + +@node Customizing Eglot +@chapter Customizing Eglot +@cindex customizing Eglot + +A large part of customizing Eglot to your needs and preferences should +actually be done via options of the Emacs packages and features which +Eglot supports and enhances (@pxref{Eglot Features}). For example: + +@itemize @bullet +@item +To configure the face used for server-derived errors and warnings, +customize the Flymake faces @code{flymake-error} and +@code{flymake-error}. + +@item +To configure the amount of space taken up by documentation in the +echo area, the customize the ElDoc variable +@code{eldoc-echo-area-use-multiline-p}. + +@item +To completely change how ElDoc displays the at-point documentation +destination, customize the ElDoc variable +@code{eldoc-display-functions}. +@end itemize + +For this reason, this manual describes only how to customize the +Eglot's own operation, which mainly has to do with the server +connections and the server features to be used by Eglot. + +@c @table, not @vtable, because some of the variables are indexed +@c elsewhere +@table @code +@item eglot-server-programs +This variable determines which language server to start for each +supported major mode, and how to invoke that server's program. +@xref{Setting Up LSP Server}, for the details. + +@vindex eglot-strict-mode +@item eglot-strict-mode: +This is @code{nil} by default, meaning that Eglot is generally lenient +about non-conforming servers. If you need to debug a server, set this +to @w{@code{(disallow-non-standard-keys enforce-required-keys)}}. + +@vindex eglot-server-initialized-hook +@item eglot-server-initialized-hook +A hook run after the server object is successfully initialized. + +@vindex eglot-connect-hook +@item eglot-connect-hook +A hook run after connection to the server is successfully +established. @xref{Starting Eglot}. + +@item eglot-managed-mode-hook +A hook run after Eglot started or stopped managing a buffer. +@xref{Eglot and Buffers}, for details of its usage. + +@vindex eglot-stay-out-of +@item eglot-stay-out-of +This variable's value lists Emacs features that Eglot shouldn't +automatically try to manage on user's behalf. It is useful, for +example, when you need to use non-LSP Flymake or Company back-ends. +To have Eglot stay away of some Emacs feature, add that feature's +symbol or a regexp that will match a symbol's name to the list: for +example, the symbol @code{xref} to leave Xref alone, or the string +@samp{company} to stay away of your Company customizations. Here's an +example: + +@lisp +(add-to-list 'eglot-stay-out-of 'flymake) +@end lisp + +Note that you can still configure the excluded Emacs features manually +to use Eglot in your @code{eglot-managed-mode-hook} or via some other +mechanism. + +@vindex eglot-workspace-configuration +@cindex server workspace configuration +@item eglot-workspace-configuration +This variable is meant to be set in the @file{.dir-locals.el} file, to +provide per-project settings, as described below in more detail. +@end table + +Some language servers need to know project-specific settings, which +the LSP calls @dfn{workspace configuration}. Eglot allows such fine +tuning of per-project settings via the variable +@code{eglot-workspace-configuration}. Eglot sends the portion of the +settings contained in this variable to each server for which such +settings were defined in the variable. These settings are +communicated to the server initially (upon establishing the +connection) or when the settings are changed, or in response to the +configuration request from the server. + +In many cases, servers can be configured globally using a +configuration file in the user's home directory or in the project +directory, which the language server reads. For example, the +@command{pylsp} server for Python reads the file +@file{~/.config/pycodestyle} and the @command{clangd} server reads the +file @file{.clangd} anywhere in the current project's directory tree. +If possible, we recommend to use these configuration files that are +independent of Eglot and Emacs; they have the advantage that they will +work with other LSP clients as well. + +If you do need to provide Emacs-specific configuration for a language +server, we recommend to define the appropriate value in the +@file{.dir-locals.el} file in the project's directory. The value of +this variable should be a property list of the following format: + +@lisp + (:@var{server} @var{plist}@dots{}) +@end lisp + +@noindent +Here @code{:@var{server}} identifies a particular language server and +@var{plist} is the corresponding keyword-value property list of one or +more parameter settings for that server. That list of parameters is +serialized to JSON by Eglot and sent to the server. For that reason +JSON values @code{true}, @code{false}, and @code{@{@}} should be +represented in the property lists as Lisp symbols @code{t}, +@code{:json-false}, and @code{nil}, respectively. + +@findex eglot-show-workspace-configuration +When experimenting with workspace settings, you can use the command +@kbd{M-x eglot-show-workspace-configuration} to inspect and debug the +JSON value to be sent to the server. This helper command works even +before actually connecting to the server. + +Here's an example of defining the workspace-configuration settings for +a project that uses two different language servers, one for Python, +whose server is @command{pylsp}, the other one for Go, with +@command{gopls} as its server (presumably, the project is written in a +combination of these two languages): + +@lisp +((python-mode + . ((eglot-workspace-configuration + . (:pylsp (:plugins (:jedi_completion (:include_params t + :fuzzy t) + :pylint (:enabled :json-false))))))) + (go-mode + . ((eglot-workspace-configuration + . (:gopls (:usePlaceholders t)))))) +@end lisp + +@noindent +This should go into the @file{.dir-locals.el} file in the project's +root directory. It sets up the value of +@code{eglot-workspace-configuration} separately for each major mode. + +Alternatively, the same configuration could be defined as follows: + +@lisp +((nil + . ((eglot-workspace-configuration + . (:pylsp (:plugins (:jedi_completion (:include_params t + :fuzzy t) + :pylint (:enabled :json-false))) + :gopls (:usePlaceholders t)))))) +@end lisp + +This is an equivalent setup which sets the value for all the +major-modes inside the project; Eglot will use for each server only +the section of the parameters intended for that server + +As yet another alternative, you can set the value of +@code{eglot-workspace-configuration} programmatically, via the +@code{dir-locals-set-class-variables} function, @pxref{Directory Local +Variables,,, elisp, GNU Emacs Lisp Reference Manual}. + +Finally, if one needs to determine the workspace configuration based +on some dynamic context, @code{eglot-workspace-configuration} can be +set to a function. The function is called with the +@code{eglot-lsp-server} instance of the connected server (if any) and +with @code{default-directory} set to the root of the project. The +function should return a value of the form described above. + +Some servers need special hand-holding to operate correctly. If your +server has some quirks or non-conformity, it's possible to extend +Eglot via Elisp to adapt to it, by defining a suitable +@code{eglot-initialization-options} method via @code{cl-defmethod} +(@pxref{Generic Functions,,, elisp, GNU Emacs Lisp Reference Manual}). + +Here's an example: + +@lisp +(add-to-list 'eglot-server-programs + '((c++ mode c-mode) . (eglot-cquery "cquery"))) + +(defclass eglot-cquery (eglot-lsp-server) () + :documentation "A custom class for cquery's C/C++ langserver.") + +(cl-defmethod eglot-initialization-options ((server eglot-cquery)) + "Passes through required cquery initialization options" + (let* ((root (car (project-roots (eglot--project server)))) + (cache (expand-file-name ".cquery_cached_index/" root))) + (list :cacheDirectory (file-name-as-directory cache) + :progressReportFrequencyMs -1))) +@end lisp + +@noindent +See the doc string of @code{eglot-initialization-options} for more +details. +@c FIXME: The doc string of eglot-initialization-options should be +@c enhanced and extended. + +@node Troubleshooting Eglot +@chapter Troubleshooting Eglot +@cindex troubleshooting Eglot + +This section documents commands and variables that can be used to +troubleshoot Eglot problems. It also provides guidelines for +reporting Eglot bugs in a way that facilitates their resolution. + +When you encounter problems with Eglot, try first using the commands +@kbd{M-x eglot-events-server} and @kbd{M-x eglot-stderr-buffer}. They +pop up special buffers that can be used to inspect the communications +between the Eglot and language server. In many cases, this will +indicate the problems or at least provide a hint. + +A common and easy-to-fix cause of performance problems is the length +of these two buffers. If Eglot is operating correctly but slowly, +customize the variable @code{eglot-events-buffer-size} (@pxref{Eglot +Variables}) to limit logging, and thus speed things up. + +If you need to report an Eglot bug, please keep in mind that, because +there are so many variables involved, it is generally both very +@emph{difficult} and @emph{absolutely essential} to reproduce bugs +exactly as they happened to you, the user. Therefore, every bug +report should include: + +@enumerate +@item +The transcript of events obtained from the buffer popped up by +@kbd{M-x eglot-events-buffer}. If the transcript can be narrowed down +to show the problematic exchange, so much the better. This is +invaluable for the investigation and reproduction of the problem. + +@item +If Emacs signaled an error (an error message was seen or heard), make +sure to repeat the process after toggling @code{debug-on-error} on +(via @kbd{M-x toggle-debug-on-error}). This normally produces a +backtrace of the error that should also be attached to the bug report. + +@item +An explanation how to obtain and install the language server you used. +If possible, try to replicate the problem with the C/C@t{++} or Python +servers, as these are very easy to install. + +@item +A description of how to setup the @emph{minimal} project (one or two +files and their contents) where the problem happens. + +@item +A recipe to replicate the problem with @emph{a clean Emacs run}. This +means @kbd{emacs -Q} invocation or a very minimal (no more that 10 +lines) @file{.emacs} initialization file. @code{eglot-ensure} and +@code{use-package} calls are generally @emph{not} needed. + +@item +Make sure to double check all the above elements and re-run the +recipe to see that the problem is reproducible. +@end enumerate + +Please keep in mind that some problems reported against Eglot may +actually be bugs in the language server or the Emacs feature/package +that used Eglot to communicate with the language server. + +@node GNU Free Documentation License +@appendix GNU Free Documentation License +@include doclicense.texi + +@node Index +@unnumbered Index +@printindex cp + +@bye From b92a5174939fba17ffb5235dd926c7063c13b1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 18 Oct 2022 12:46:48 +0100 Subject: [PATCH 728/771] Minor stylistic fixes to introduction of doc/misc/eglot.texi * doc/misc/eglot.texi (title): Add "the". (Top): Fix sentence structure in top-level introduction. --- doc/misc/eglot.texi | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 761964334bb..df8509aa218 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -1,7 +1,7 @@ \input texinfo @c -*-texinfo-*- @c %**start of header @setfilename ../../eglot.info -@settitle Eglot: The Emacs Client for Language Server Protocol +@settitle Eglot: The Emacs Client for the Language Server Protocol @include docstyle.texi @syncodeindex vr cp @syncodeindex fn cp @@ -60,24 +60,24 @@ modify this GNU manual.'' @cindex LSP @cindex language server protocol -Eglot is the Emacs client for Language Server Protocol. The name -``Eglot'' is an acronym that stands for ``@emph{E}macs -Poly@emph{glot}''.@footnote{ -A @dfn{polyglot} is a person who is able to use several languages. +Eglot is the Emacs client for the @dfn{Language Server Protocol} +(@acronym{LSP}). The name ``Eglot'' is an acronym that stands for +``@emph{E}macs Poly@emph{glot}''.@footnote{ +A @dfn{polyglot} is a +person who is able to use several languages. } Eglot provides infrastructure and a set of commands for enriching -Emacs source code editing capabilities using the @dfn{Language Server -Protocol} (@acronym{LSP}) -- a standardized communications protocol -between source code editors (such as Emacs) and language servers -- -programs external to Emacs that analyze the source code on behalf of -Emacs. The protocol allows Emacs to receive various source code -services from the server, such as description and location of -functions calls, types of variables, class definitions, syntactic -errors, etc. This way, Emacs doesn't need to implement the -language-specific parsing and analysis capabilities in its own code, -but is still capable of providing sophisticated editing features that -rely on such capabilities, such as automatic code completion, go-to -definition of function/class, documentation of symbol at-point, -refactoring, on-the-fly diagnostics, and more. +the source code editing capabilities of Emacs via LSP. LSP is a +standardized communications protocol between source code editors (such +as Emacs) and language servers, programs external to Emacs for +analyzing source code on behalf of Emacs. The protocol allows Emacs +to receive various source code services from the server, such as +description and location of functions calls, types of variables, class +definitions, syntactic errors, etc. This way, Emacs doesn't need to +implement the language-specific parsing and analysis capabilities in +its own code, but is still capable of providing sophisticated editing +features that rely on such capabilities, such as automatic code +completion, go-to definition of function/class, documentation of +symbol at-point, refactoring, on-the-fly diagnostics, and more. Eglot itself is completely language-agnostic, but it can support any programming language for which there is a language server and an Emacs From c681f374788235cbaf1dca062450202e90fd2a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Tue, 18 Oct 2022 14:46:27 +0100 Subject: [PATCH 729/771] More minor fixes to doc/misc/eglot.texi Most, if not all of these, were previously discussed with Eli. * doc/misc/eglot.texi (Setting Up LSP Server): Fix repetition of "This variable". (Setting Up LSP Server): Explain that single a running instance supports multiple major modes. (Starting Eglot, Eglot and Buffers): Correctly describe mode-line indication. (Eglot Features): Suggest that company-mode is just one of the possible packages. Explain that Eglot arranges for the completion package to "instantiate" snippets. Could have used "expand". Mention benefits of having the popular markdown-mode available. (Eglot Commands): Explain how eglot-reconnect and eglot-clear-status are useful. (Eglot Variables): Clarify when eglot-autoreconnect is useful. Clarify how eglot-sync-connect and eglot-connect-timeout relate to each other. Clarify semantics of eglot-confirm-server-initiated-edits. --- doc/misc/eglot.texi | 98 ++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index df8509aa218..3e79e89492d 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -191,9 +191,9 @@ provided by the @code{eglot-server-programs} variable. @defvar eglot-server-programs This variable associates major modes with names and command-line arguments of the language server programs corresponding to the -programming language of each major mode. This variable provides all -the information that Eglot needs to know about the programming -language of the source you are editing. +programming language of each major mode. It provides all the +information that Eglot needs to know about the programming language of +the source you are editing. The value of the variable is an alist, whose elements are of the form @w{@code{(@var{major-mode} . @var{server})}}. @@ -205,8 +205,9 @@ and @var{id} a string that identifies the language to the server (if Eglot cannot by itself convert the major-mode to the language identifier string required by the server). In addition, @var{major-mode} can be a list of several major mode specified in one -of the above forms -- this means the server can support more than one -major mode. +of the above forms -- this means a running instance of the associated +server is responsible for files of multiple major modes or languages +in the project. The @var{server} part of the alist elements can be one of the following: @@ -280,17 +281,14 @@ The most common way to start Eglot is to simply visit a source file of a given language and use the command @kbd{M-x eglot}. This starts the language server suitable for the visited file's major-mode, and attempts to connect to it. If the connection to the language server -is successful, you will see the @code{eglot:@var{server}} indicator on -the mode line which reflects the server that was started. If the +is successful, you will see the @code{[eglot:@var{project}]} indicator +on the mode line which reflects the server that was started. If the server program couldn't be started or connection to it failed, you will see an error message; in that case, try to troubleshoot the -problem as described in @ref{Troubleshooting Eglot}. -@c FIXME: Is the mode-line indication just eglot:server, or -@c egloit:serve/project, as described farther down? - -Once a language server was successfully started and Eglot connected to -it, you can immediately start using the Emacs features supported by -Eglot, as described in @ref{Eglot Features}. +problem as described in @ref{Troubleshooting Eglot}. Once a language +server was successfully started and Eglot connected to it, you can +immediately start using the Emacs features supported by Eglot, as +described in @ref{Eglot Features}. A single Eglot session for a certain major-mode usually serves all the buffers under that mode which visit files from the same project, so @@ -417,16 +415,22 @@ to what the @code{eglot-format} command does (see below), but is activated automatically as you type. @item -If @code{company-mode}, a popular 3rd-party completion package, is -installed, Eglot enhances it by providing completion candidates based -on the language-server analysis of the source code. +If a completion package such as @code{company-mode}, a popular +3rd-party completion package, is installed, Eglot enhances it by +providing completion candidates based on the language-server analysis +of the source code. @item If @code{yasnippet}, a popular package for automatic insertion of code -templates, is installed, and the language server supports -template-based completion, Eglot provides to @code{yasnippet} -completion templates based on the language-server analysis and -knowledge bases. +templates (snippets), is installed, and the language server supports +snippet completion candidates, Eglot arranges for the completion +package to instantiate these snippets using @code{yasnippet}. + +@item +If the popular package @code{markdown-mode} is installed, and the +server provides at-point documentation formatted as Markdown in +addition to plain text, Eglot arranges for the ElDoc package to enrich +this text with e.g. fontification before displaying it to the user. @item In addition to enabling and enhancing other features and packages, @@ -547,7 +551,7 @@ are served by a single language-server connection. (If the project uses several programming languages, there will usually be a separate server connection for each group of files written in the same language and using the same Emacs major-mode.) Eglot adds the -@samp{eglot:@var{server}/@var{project}} indication to the mode line of +@samp{[eglot:@var{project}]} indication to the mode line of each such buffer, where @var{server} is the name of the server and @var{project} identifies the project by its root directory. Clicking the mouse on the Eglot mode-line indication activates a menu with @@ -630,8 +634,10 @@ Emacs features will be configured to use Eglot, use the @code{eglot-stay-out-of} option (@pxref{Customizing Eglot}). @item M-x eglot-reconnect -@c FIXME: When and why would this command be useful? Move to less common? -Reconnect to current language server. +Shuts down an the current connection to the language server and +immediately restarts it using the same options used originally. This +can sometimes be useful to unclog a partially malfunctioning server +connection. @item M-x eglot-shutdown Shut down a language server. This commands prompts for a language @@ -706,7 +712,13 @@ the current value of the variable @code{eglot-workspace-configuration} @item M-x eglot-clear-status Clear the last JSONRPC error for the server of the current buffer. -@c FIXME: Better description of the need and the effect. +Eglot keeps track of erroneous situations encountered by the server in +its mode-line indication so that the user may inspect the +communication leading up to it (@pxref{Troubleshooting Eglot}). If +the situation is deemed uninteresting or temporary, this command can +be used to ``forget'' the error. Note that the command @code{M-x +eglot-reconnect} can sometimes be used to unclog a temporarily +malfunctioning server. @end ftable As described in @ref{Eglot Features} most features associated with @@ -757,13 +769,12 @@ This section provides a reference of the Eglot' user options. @vtable @code @item eglot-autoreconnect This option controls the ability to reconnect automatically to the -language server. The default value 3 means to attempt reconnection -only if the previous successful connection lasted for more than that -number of seconds; a different positive value changes the minimal -length of the connection to trigger reconnection. A value of @code{t} -means always reconnect automatically, and @code{nil} means never -reconnect. The alternative to automatic reconnection is the command -@code{eglot-reconnect} (@pxref{Eglot Commands}). +language server when Eglot detects that the server process terminated +unexpectedly. The default value 3 means to attempt reconnection only +if the previous successful connection lasted for more than that number +of seconds; a different positive value changes the minimal length of +the connection to trigger reconnection. A value of @code{t} means +always reconnect automatically, and @code{nil} means never reconnect. @item eglot-connect-timeout This specifies the number of seconds before connection attempt to a @@ -771,14 +782,14 @@ language server times out. The value of @code{nil} means never time out. The default is 30 seconds. @item eglot-sync-connect -This controls whether attempts to connect to language servers should -be blocking. The setting is mainly important for slow connections. -The default value is 3; a positive value means block for that many -seconds, then wait for the connection in the background. The value -of @code{t} means block for @code{eglot-connect-timeout} seconds. The -value of @code{nil} or zero means don't block at all. -@c FIXME: the code doesn't use eglot-connect-timeout, it uses a -@c hard-coded value of 30. +This setting is mainly important for connections which are slow to +establish. Whereas the variable @code{eglot-connect-timeout} controls +how long to wait for, this variable controls whether to block Emacs's +user interface while waiting. The default value is 3; a positive +value means block for that many seconds, then wait for the connection +in the background. The value of @code{t} means block during the whole +waiting period. The value of @code{nil} or zero means don't block at +all during the waiting period. @item eglot-events-buffer-size This determines the size of the Eglot events buffer. @xref{Eglot @@ -800,11 +811,8 @@ The default is @code{nil}; if you want to shut down a server, use Various Eglot commands and code actions result in the language server sending editing commands to Emacs. If this option's value is non-@code{nil} (the default), Eglot will ask for confirmation before -performing the edits requested by the server. -@c FIXME: Not clear: is the confirmation required for each individual -@c edit, or for as group? for each buffer or just once? And what -@c about on-type reformatting -- does that require confirmation as -@c well (which would be annoying)? +performing edits initiated by the server or edits whose scope affects +buffers other than the one where the user initiated the request. @item eglot-ignored-server-capabilities This variable's value is a list of language server capabilities that From 254f4766e6ef5370f75530752db5640895b9066a Mon Sep 17 00:00:00 2001 From: Stefan Monnier Date: Tue, 18 Oct 2022 16:06:42 -0400 Subject: [PATCH 730/771] functions.texi: Fix bug#58602 * doc/lispref/functions.texi (Function Documentation): Document `:documentation` and `function-documentation`. --- doc/lispref/functions.texi | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/doc/lispref/functions.texi b/doc/lispref/functions.texi index 8b858e0aa01..e1102832529 100644 --- a/doc/lispref/functions.texi +++ b/doc/lispref/functions.texi @@ -533,6 +533,44 @@ Instead, use the @code{advertised-calling-convention} declaration compiler emit a warning message when it compiles Lisp programs which use the deprecated calling convention. +@cindex computed documentation string +@kindex{:documentation} +Documentation strings are usually static, but occasionally it can be +necessary to generate them dynamically. In some cases you can do so +by writing a macro which generates at compile time the code of the +function, including the desired documentation string. But you can +also generate the docstring dynamically by writing +@code{(:documentation @var{form})} instead of the documentation +string. This will evaluate @var{form} at run-time when the function +is defined and use it as the documentation string@footnote{This only +works in code using @code{lexical-binding}.}. You can also compute +the documentation string on the fly when it is requested, by setting +the @code{function-documentation} property of the function's symbol to +a Lisp form that evaluates to a string. + +For example: +@example +@group +(defun adder (x) + (lambda (y) + (:documentation (format "Add %S to the argument Y." x)) + (+ x y))) +(defalias 'adder5 (adder 5)) +(documentation 'adder5) + @result{} "Add 5 to the argument Y." +@end group + +@group +(put 'adder5 'function-documentation + '(concat (documentation (symbol-function 'adder5) 'raw) + " Consulted at " (format-time-string "%H:%M:%S"))) +(documentation 'adder5) + @result{} "Add 5 to the argument Y. Consulted at 15:52:13" +(documentation 'adder5) + @result{} "Add 5 to the argument Y. Consulted at 15:52:18" +@end group +@end example + @node Function Names @section Naming a Function @cindex function definition From 2cca6408fde59e57a0937e561675d181f7fa226e Mon Sep 17 00:00:00 2001 From: Lars Ingebrigtsen Date: Tue, 18 Oct 2022 22:17:12 +0200 Subject: [PATCH 731/771] Fix functions.texi syntax error * doc/lispref/functions.texi (Function Documentation): Fix syntax error. --- doc/lispref/functions.texi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/lispref/functions.texi b/doc/lispref/functions.texi index e1102832529..7ffde7d43d1 100644 --- a/doc/lispref/functions.texi +++ b/doc/lispref/functions.texi @@ -534,7 +534,7 @@ compiler emit a warning message when it compiles Lisp programs which use the deprecated calling convention. @cindex computed documentation string -@kindex{:documentation} +@kindex :documentation Documentation strings are usually static, but occasionally it can be necessary to generate them dynamically. In some cases you can do so by writing a macro which generates at compile time the code of the From 155ddde4dd3f6246814ab76bc2f54f4d571bbd15 Mon Sep 17 00:00:00 2001 From: Stefan Monnier Date: Tue, 18 Oct 2022 18:43:50 -0400 Subject: [PATCH 732/771] (sit-for): Add compiler-macro to warn about obsolete calling convention * lisp/subr.el (sit-for): Add compiler-macro. * lisp/eshell/esh-util.el (eshell-redisplay): * lisp/play/zone.el (zone, zone-pgm-jitter, zone-pgm-whack-chars): (zone-remove-text): Avoid obsolete calling convention. --- lisp/eshell/esh-util.el | 2 +- lisp/play/zone.el | 8 ++++---- lisp/subr.el | 9 ++++++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lisp/eshell/esh-util.el b/lisp/eshell/esh-util.el index 9b464a0a137..f47373c115f 100644 --- a/lisp/eshell/esh-util.el +++ b/lisp/eshell/esh-util.el @@ -455,7 +455,7 @@ list." ;; runs while point is in the minibuffer and the users attempt ;; to use completion. Don't ask me. (condition-case nil - (sit-for 0 0) + (sit-for 0) (error nil))) (defun eshell-read-passwd-file (file) diff --git a/lisp/play/zone.el b/lisp/play/zone.el index 5ea5bbc9267..e3a9507f1cc 100644 --- a/lisp/play/zone.el +++ b/lisp/play/zone.el @@ -139,7 +139,7 @@ run a specific program. The program must be a member of (untabify (point-min) (point-max)) (set-window-start (selected-window) (point-min)) (set-window-point (selected-window) wp) - (sit-for 0 500) + (sit-for 0.500) (let ((ct (and f (frame-parameter f 'cursor-type))) (show-trailing-whitespace nil) restore) @@ -249,7 +249,7 @@ run a specific program. The program must be a member of (while (not (input-pending-p)) (funcall (elt ops (random (length ops)))) (goto-char (point-min)) - (sit-for 0 10)))) + (sit-for 0.01)))) ;;;; whacking chars @@ -262,7 +262,7 @@ run a specific program. The program must be a member of (aset tbl i (+ 48 (random (- 123 48)))) (setq i (1+ i))) (translate-region (point-min) (point-max) tbl) - (sit-for 0 2))))) + (sit-for 0.002))))) (put 'zone-pgm-whack-chars 'wc-tbl (let ((tbl (make-string 128 ?x)) @@ -290,7 +290,7 @@ run a specific program. The program must be a member of (delete-char 1) (insert " "))) (forward-char 1)))) - (sit-for 0 2)))) + (sit-for 0.002)))) (defun zone-pgm-dissolve () (zone-remove-text) diff --git a/lisp/subr.el b/lisp/subr.el index 08dfe7aa430..e49c22158f9 100644 --- a/lisp/subr.el +++ b/lisp/subr.el @@ -3270,7 +3270,14 @@ An obsolete, but still supported form is where the optional arg MILLISECONDS specifies an additional wait period, in milliseconds; this was useful when Emacs was built without floating point support." - (declare (advertised-calling-convention (seconds &optional nodisp) "22.1")) + (declare (advertised-calling-convention (seconds &optional nodisp) "22.1") + (compiler-macro + (lambda (form) + (if (not (or (numberp nodisp) obsolete)) form + (macroexp-warn-and-return + "Obsolete calling convention for 'sit-for'" + `(,(car form) (+ ,seconds (/ (or ,nodisp 0) 1000.0)) ,obsolete) + '(obsolete sit-for)))))) ;; This used to be implemented in C until the following discussion: ;; https://lists.gnu.org/r/emacs-devel/2006-07/msg00401.html ;; Then it was moved here using an implementation based on an idle timer, From 620f18c489ff9ce3d0a4afe04d438a1bc0525e73 Mon Sep 17 00:00:00 2001 From: Randy Taylor Date: Mon, 17 Oct 2022 21:29:30 -0400 Subject: [PATCH 733/771] ; * src/xterm.c (mark_xterm): Fix x11 with i18n build --- src/xterm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xterm.c b/src/xterm.c index 7c3ab87e87b..3075b5af230 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -30281,7 +30281,7 @@ mark_xterm (void) { Lisp_Object val; #if defined HAVE_XINPUT2 || defined USE_TOOLKIT_SCROLL_BARS \ - || defined HAVE_XRANDR || defined USE_GTK + || defined HAVE_XRANDR || defined USE_GTK || defined HAVE_X_I18N struct x_display_info *dpyinfo; #if defined HAVE_XINPUT2 || defined USE_TOOLKIT_SCROLL_BARS int i; From 5247a72aecb417e2f71e37af40ebacdfc26158b7 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Wed, 19 Oct 2022 08:02:36 +0200 Subject: [PATCH 734/771] * lib-src/rcs2log: Add fallback for $TMPDIR. --- lib-src/rcs2log | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib-src/rcs2log b/lib-src/rcs2log index bc7875cfdd2..2a72404d9e5 100755 --- a/lib-src/rcs2log +++ b/lib-src/rcs2log @@ -209,7 +209,7 @@ month_data=' if type mktemp >/dev/null 2>&1; then logdir=`mktemp -d` else - logdir=$TMPDIR/rcs2log$$ + logdir="${TMPDIR-/tmp}/rcs2log$$" (umask 077 && mkdir "$logdir") fi || exit case $logdir in From a9111d8670b48f473e968a0e75d83782dbf74425 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Wed, 19 Oct 2022 13:26:54 +0200 Subject: [PATCH 735/771] Add admin/emacs-shell-lib for shared bash code * admin/emacs-shell-lib: New file for shared bash code. * admin/automerge: * admin/diff-tar-files: * admin/emacs-shell-lib: * admin/make-manuals: * admin/update_autogen: * admin/upload-manuals: Simplify and improve using above new library. --- admin/automerge | 23 +----------- admin/diff-tar-files | 8 ++-- admin/emacs-shell-lib | 87 +++++++++++++++++++++++++++++++++++++++++++ admin/make-manuals | 13 +------ admin/update_autogen | 20 +--------- admin/upload-manuals | 10 +---- 6 files changed, 98 insertions(+), 63 deletions(-) create mode 100644 admin/emacs-shell-lib diff --git a/admin/automerge b/admin/automerge index c7c17dfb5ec..d2c92948e17 100755 --- a/admin/automerge +++ b/admin/automerge @@ -35,18 +35,7 @@ ## it with the -d option in the repository directory, in case a pull ## updates this script while it is working. -set -o nounset - -die () # write error to stderr and exit -{ - [ $# -gt 0 ] && echo "$PN: $*" >&2 - exit 1 -} - -PN=${0##*/} # basename of script -PD=${0%/*} - -[ "$PD" = "$0" ] && PD=. # if PATH includes PWD +source "${0%/*}/emacs-shell-lib" usage () { @@ -129,13 +118,7 @@ OPTIND=1 [ "$test" ] && build=1 -if [ -x "$(command -v mktemp)" ]; then - tempfile=$(mktemp "/tmp/$PN.XXXXXXXXXX") -else - tempfile=/tmp/$PN.$$ -fi - -trap 'rm -f $tempfile 2> /dev/null' EXIT +tempfile="$(emacs_mktemp)" [ -e Makefile ] && [ "$build" ] && { @@ -263,5 +246,3 @@ git push || die "push error" exit 0 - -### automerge ends here diff --git a/admin/diff-tar-files b/admin/diff-tar-files index 6ab39eab2f5..869c9421502 100755 --- a/admin/diff-tar-files +++ b/admin/diff-tar-files @@ -1,4 +1,4 @@ -#! /bin/sh +#!/bin/bash # Copyright (C) 2001-2022 Free Software Foundation, Inc. @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with GNU Emacs. If not, see . +source "${0%/*}/emacs-shell-lib" if [ $# != 2 ]; then cat < $old_tmp tar tf "$new_tar" | sed -e 's,^[^/]*,,' | sort > $new_tmp diff --git a/admin/emacs-shell-lib b/admin/emacs-shell-lib new file mode 100644 index 00000000000..750f81e0577 --- /dev/null +++ b/admin/emacs-shell-lib @@ -0,0 +1,87 @@ +#!/bin/bash +### emacs-shell-lib - shared code for Emacs shell scripts + +## Copyright (C) 2022 Free Software Foundation, Inc. + +## Author: Stefan Kangas + +## This file is part of GNU Emacs. + +## GNU Emacs is free software: you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. + +## GNU Emacs is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +## You should have received a copy of the GNU General Public License +## along with GNU Emacs. If not, see . + +### Code: + +# Set an explicit umask. +umask 077 + +# Treat unset variables as an error. +set -o nounset + +# Exit immediately on error. +set -o errexit + +# Avoid non-standard command output from non-C locales. +unset LANG LC_ALL LC_MESSAGES + +PN=${0##*/} # basename of script +PD=${0%/*} # script directory + +[ "$PD" = "$0" ] && PD=. # if PATH includes PWD + +die () # write error to stderr and exit +{ + [ $# -gt 0 ] && echo "$PN: $@" >&2 + exit 1 +} + +emacs_tempfiles=() + +emacs_tempfiles_cleanup () +{ + for file in ${emacs_tempfiles[@]}; do + rm -f "${file}" 2> /dev/null + done +} + +trap ' + ret=$? + emacs_tempfiles_cleanup + exit $ret +' EXIT + +emacs_mktemp () +{ + local readonly file="${1-}" + local tempfile + local prefix + + if [ -z "$file" ]; then + prefix="$PN" + else + prefix="$1" + fi + + if [ -x "$(command -v mktemp)" ]; then + tempfile=$(mktemp "${TMPDIR-/tmp}/${prefix}.XXXXXXXXXX") + else + tempfile="${TMPDIR-/tmp}/${prefix}.$RANDOM$$" + (umask 077 && touch "$tempfile") + fi + + [ -z "${tempfile}" ] && die "Creating temporary file failed" + + emacs_tempfiles+=("${tempfile}") + + echo "$tempfile" +} diff --git a/admin/make-manuals b/admin/make-manuals index cb0c00a423f..a252bf20f1e 100755 --- a/admin/make-manuals +++ b/admin/make-manuals @@ -33,15 +33,7 @@ ### Code: -set -o nounset - -die () # write error to stderr and exit -{ - [ $# -gt 0 ] && echo "$PN: $@" >&2 - exit 1 -} - -PN=${0##*/} # basename of script +source "${0%/*}/emacs-shell-lib" usage () { @@ -96,8 +88,7 @@ OPTIND=1 [ -e admin/admin.el ] || die "admin/admin.el not found" -tempfile=/tmp/$PN.$$ -trap "rm -f $tempfile 2> /dev/null" EXIT +tempfile="$(emacs_mktemp)" [ "$continue" ] || rm -rf $outdir diff --git a/admin/update_autogen b/admin/update_autogen index d1f49d9f25e..55e11be95c7 100755 --- a/admin/update_autogen +++ b/admin/update_autogen @@ -32,18 +32,7 @@ ### Code: -set -o nounset - -die () # write error to stderr and exit -{ - [ $# -gt 0 ] && echo "$PN: $@" >&2 - exit 1 -} - -PN=${0##*/} # basename of script -PD=${0%/*} - -[ "$PD" = "$0" ] && PD=. # if PATH includes PWD +source "${0%/*}/emacs-shell-lib" ## This should be the admin directory. cd $PD || exit @@ -102,10 +91,7 @@ done [ "$basegen" ] || die "internal error" -tempfile=/tmp/$PN.$$ - -trap 'rm -f $tempfile 2> /dev/null' EXIT - +tempfile="$(emacs_mktemp)" while getopts ":hcfqA:CL" option ; do case $option in @@ -312,5 +298,3 @@ commit "loaddefs" $modified || die "commit error" exit 0 - -### update_autogen ends here diff --git a/admin/upload-manuals b/admin/upload-manuals index 50336ee64c0..04f7c3acc72 100755 --- a/admin/upload-manuals +++ b/admin/upload-manuals @@ -36,15 +36,7 @@ ### Code: -set -o nounset - -die () # write error to stderr and exit -{ - [ $# -gt 0 ] && echo "$PN: $@" >&2 - exit 1 -} - -PN=${0##*/} # basename of script +source "${0%/*}/emacs-shell-lib" usage () { From 45ca261c98af5ff29c16b911bee357081c559cf6 Mon Sep 17 00:00:00 2001 From: Arun Isaac Date: Tue, 18 Oct 2022 23:30:59 +0530 Subject: [PATCH 736/771] Add tamil99 input method (bug#58070) * lisp/leim/quail/indian.el: Require pcase and seq. ("tamil99"): New input method. * etc/NEWS: Mention new tamil99 input method. --- etc/NEWS | 3 + lisp/leim/quail/indian.el | 161 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/etc/NEWS b/etc/NEWS index dbc8971e415..2f7746b7f0b 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1357,6 +1357,9 @@ The default input method for the Tamil language environment is now change the input method's translation rules, customize the user option 'tamil-translation-rules'. +--- +*** New tamil99 input method for the Tamil language + * Changes in Specialized Modes and Packages in Emacs 29.1 diff --git a/lisp/leim/quail/indian.el b/lisp/leim/quail/indian.el index 048e16e8d80..17af8356bbf 100644 --- a/lisp/leim/quail/indian.el +++ b/lisp/leim/quail/indian.el @@ -30,6 +30,8 @@ ;;; Code: +(require 'pcase) +(require 'seq) (require 'quail) (require 'ind-util) @@ -699,6 +701,165 @@ is." "tamil-inscript-digits" "Tamil" "TmlISD" "Tamil keyboard Inscript with Tamil digits support.") +;; Tamil99 input method +;; +;; Tamil99 is a keyboard layout and input method that is specifically +;; designed for the Tamil language. Vowels and vowel modifiers are +;; input with your left hand, and consonants are input with your right +;; hand. See https://en.wikipedia.org/wiki/Tamil_99 +;; +;; தமிழ்99 உள்ளீட்டு முறை +;; +;; தமிழ்99 தமிழுக்கென்றே உருவாக்கப்பட்ட விசைப்பலகை அமைப்பும் உள்ளீட்டு முறையும் +;; ஆகும். உயிர்களை இடக்கையுடனும் மெய்களை வலக்கையுடனும் தட்டச்சிடும்படி +;; அமைக்கப்பட்டது. https://ta.wikipedia.org/wiki/%E0%AE%A4%E0%AE%AE%E0%AE%BF%E0%AE%B4%E0%AF%8D_99 +;; காண்க. + +(quail-define-package + "tamil99" "Tamil" "தமிழ்99" + t "Tamil99 input method" + nil t t t t nil nil nil nil nil t) + +(defconst tamil99-vowels + '(("q" "ஆ") + ("w" "ஈ") + ("e" "ஊ") + ("r" "ஐ") + ("t" "ஏ") + ("a" "அ") + ("s" "இ") + ("d" "உ") + ("g" "எ") + ("z" "ஔ") + ("x" "ஓ") + ("c" "ஒ")) + "Mapping for vowels.") + +(defconst tamil99-vowel-modifiers + '(("q" "ா") + ("w" "ீ") + ("e" "ூ") + ("r" "ை") + ("t" "ே") + ("a" "") + ("s" "ி") + ("d" "ு") + ("g" "ெ") + ("z" "ௌ") + ("x" "ோ") + ("c" "ொ") + ("f" "்")) + "Mapping for vowel modifiers.") + +(defconst tamil99-hard-consonants + '(("h" "க") + ("[" "ச") + ("o" "ட") + ("l" "த") + ("j" "ப") + ("u" "ற")) + "Mapping for hard consonants (வல்லினம்).") + +(defconst tamil99-soft-consonants + '(("b" "ங") + ("]" "ஞ") + ("p" "ண") + (";" "ந") + ("k" "ம") + ("i" "ன")) + "Mapping for soft consonants (மெல்லினம்).") + +(defconst tamil99-medium-consonants + '(("'" "ய") + ("m" "ர") + ("n" "ல") + ("v" "வ") + ("/" "ழ") + ("y" "ள")) + "Mapping for medium consonants (இடையினம்).") + +(defconst tamil99-grantham-consonants + '(("Q" "ஸ") + ("W" "ஷ") + ("E" "ஜ") + ("R" "ஹ")) + "Mapping for grantham consonants (கிரந்தம்).") + +(defconst tamil99-consonants + (append tamil99-hard-consonants + tamil99-soft-consonants + tamil99-medium-consonants + tamil99-grantham-consonants) + "Mapping for all consonants.") + +(defconst tamil99-other + `(("T" ,(vector "க்ஷ")) + ("Y" ,(vector "ஶஂரீ")) + ("O" "[") + ("P" "]") + ("A" "௹") + ("S" "௺") + ("D" "௸") + ("F" "ஃ") + ("K" "\"") + ("L" ":") + (":" ";") + ("\"" "'") + ("Z" "௳") + ("X" "௴") + ("C" "௵") + ("V" "௶") + ("B" "௷") + ("M" "/")) + "Mapping for miscellaneous characters.") + +;; உயிர் +;; vowel +(mapc (pcase-lambda (`(,vowel-key ,vowel)) + (quail-defrule vowel-key vowel)) + tamil99-vowels) + +(mapc (pcase-lambda (`(,consonant-key ,consonant)) + ;; அகர உயிர்மெய் + ;; consonant symbol (consonant combined with the first vowel அ) + (quail-defrule consonant-key consonant) + ;; மெய்யொற்று பின் அகர உயிர்மெய் + ;; pulli on double consonant + (quail-defrule (concat consonant-key consonant-key) + (vector (concat consonant "்" consonant))) + (mapc (pcase-lambda (`(,vowel-key ,vowel-modifier)) + ;; உயிர்மெய் + ;; vowelised consonant + (quail-defrule (concat consonant-key vowel-key) + (vector (concat consonant vowel-modifier))) + ;; மெய்யொற்று பின் பிற உயிர்மெய் + ;; vowelised consonant after double consonant + (quail-defrule (concat consonant-key consonant-key vowel-key) + (vector (concat consonant "்" consonant vowel-modifier)))) + tamil99-vowel-modifiers)) + tamil99-consonants) + +(seq-mapn (pcase-lambda (`(,soft-consonant-key ,soft-consonant) + `(,hard-consonant-key ,hard-consonant)) + ;; மெல்லினம் பின் வல்லினம் + ;; hard consonant after soft consonant + (quail-defrule (concat soft-consonant-key hard-consonant-key) + (vector (concat soft-consonant "்" hard-consonant))) + (mapc (pcase-lambda (`(,vowel-key ,vowel-modifier)) + ;; மெல்லின ஒற்றொட்டிய வல்லினம் பின் உயிர்மெய் + ;; vowelised consonant after soft-hard consonant pair + (quail-defrule (concat soft-consonant-key hard-consonant-key vowel-key) + (vector (concat soft-consonant "்" hard-consonant vowel-modifier)))) + tamil99-vowel-modifiers)) + tamil99-soft-consonants + tamil99-hard-consonants) + +;; பிற வரியுருக்கள் +;; other characters +(mapc (pcase-lambda (`(,key ,translation)) + (quail-defrule key translation)) + tamil99-other) + ;; Probhat Input Method (quail-define-package "bengali-probhat" "Bengali" "BngPB" t From 084ac1e5147558b448af41fbfebc0a99a578819c Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Wed, 19 Oct 2022 14:40:11 +0300 Subject: [PATCH 737/771] ; Fix last change * lisp/leim/quail/indian.el ("tamil99"): * etc/NEWS: Minor copyedits of the tamil99 documentation. --- etc/NEWS | 4 +++- lisp/leim/quail/indian.el | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index 2f7746b7f0b..a378b5acbbc 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1358,7 +1358,9 @@ change the input method's translation rules, customize the user option 'tamil-translation-rules'. --- -*** New tamil99 input method for the Tamil language +*** New tamil99 input method for the Tamil language. +This supports the keyboard layout specifically designed for the Tamil +language. * Changes in Specialized Modes and Packages in Emacs 29.1 diff --git a/lisp/leim/quail/indian.el b/lisp/leim/quail/indian.el index 17af8356bbf..31a34bc1de2 100644 --- a/lisp/leim/quail/indian.el +++ b/lisp/leim/quail/indian.el @@ -704,7 +704,7 @@ is." ;; Tamil99 input method ;; ;; Tamil99 is a keyboard layout and input method that is specifically -;; designed for the Tamil language. Vowels and vowel modifiers are +;; designed for the Tamil language. Vowels and vowel modifiers are ;; input with your left hand, and consonants are input with your right ;; hand. See https://en.wikipedia.org/wiki/Tamil_99 ;; From 4b2c83eeaf25dc2e95b0265d2e3013ad0d16aa25 Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Wed, 19 Oct 2022 14:50:23 +0300 Subject: [PATCH 738/771] ; * doc/lispref/modes.texi (Defining Minor Modes): Explain TURN-ON. --- doc/lispref/modes.texi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/lispref/modes.texi b/doc/lispref/modes.texi index ad46708f027..72b4a6fb4f7 100644 --- a/doc/lispref/modes.texi +++ b/doc/lispref/modes.texi @@ -1839,7 +1839,9 @@ to enable or disable the buffer-local minor mode @var{mode} in all (or some; see below) buffers. It also executes the @var{body} forms. To turn on the minor mode in a buffer, it uses the function @var{turn-on}; to turn off the minor mode, it calls @var{mode} with -@minus{}1 as argument. +@minus{}1 as argument. (The function @var{turn-on} is a separate +function so it could determine whether to enable the minor mode or not +when it is not a priori clear that it should always be enabled.) Globally enabling the mode also affects buffers subsequently created by visiting files, and buffers that use a major mode other than From 7a551e92005dc9964c1a74a2896b5dbf0ca231b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 19 Oct 2022 13:21:40 +0100 Subject: [PATCH 739/771] Tweak some node names in doc/misc/eglot.texi "Shutting Down LSP Server" -> "Shutting Down LSP Servers" "Setting Up LSP Server" -> "Setting Up LSP Servers" * doc/misc/eglot.texi: Rework node names. --- doc/misc/eglot.texi | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 3e79e89492d..286607738b4 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -88,11 +88,11 @@ This manual documents how to configure, use, and customize Eglot. @insertcopying @menu -* Quick Start:: For the impatient. -* Eglot and LSP Servers:: How to work with language servers -* Using Eglot:: Important Eglot commands and variables. -* Customizing Eglot:: Eglot customization and advanced features. -* Troubleshooting Eglot:: Troubleshooting and reporting bugs. +* Quick Start:: For the impatient. +* Eglot and LSP Servers:: How to work with language servers +* Using Eglot:: Important Eglot commands and variables. +* Customizing Eglot:: Eglot customization and advanced features. +* Troubleshooting Eglot:: Troubleshooting and reporting bugs. * GNU Free Documentation License:: The license for this manual. * Index:: @end menu @@ -164,13 +164,13 @@ This chapter describes how to set up Eglot for your needs, and how to start it. @menu -* Setting Up LSP Server:: How to configure LSP servers for your needs. -* Starting Eglot:: Ways of starting Eglot for your project. -* Shutting Down LSP Server:: +* Setting Up LSP Servers:: How to configure LSP servers for your needs. +* Starting Eglot:: Ways of starting Eglot for your project. +* Shutting Down LSP Servers:: @end menu -@node Setting Up LSP Server -@section Setting Up LSP Server +@node Setting Up LSP Servers +@section Setting Up LSP Servers @cindex setting up LSP server for Eglot @cindex language server for Eglot @@ -321,8 +321,8 @@ When Eglot connects to a language server for the first time in an Emacs session, it runs the hook @code{eglot-connect-hook} (@pxref{Eglot Variables}). -@node Shutting Down LSP Server -@section Shutting Down LSP Server +@node Shutting Down LSP Servers +@section Shutting Down LSP Servers @cindex shutting down LSP server When Eglot is turned on, it arranges for turning itself off @@ -803,7 +803,7 @@ restarted using @kbd{eglot-shutdown} followed by @item eglot-autoshutdown If this is non-@code{nil}, Eglot shuts down a language server when the -last buffer managed by it is killed. @xref{Shutting Down LSP Server}. +last buffer managed by it is killed. @xref{Shutting Down LSP Servers}. The default is @code{nil}; if you want to shut down a server, use @kbd{M-x eglot-shutdown} (@pxref{Eglot Commands}). @@ -882,7 +882,7 @@ connections and the server features to be used by Eglot. @item eglot-server-programs This variable determines which language server to start for each supported major mode, and how to invoke that server's program. -@xref{Setting Up LSP Server}, for the details. +@xref{Setting Up LSP Servers}, for the details. @vindex eglot-strict-mode @item eglot-strict-mode: From 20d44771201ef96fcfd6aebffe05aa50c5fa8074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Wed, 19 Oct 2022 13:23:14 +0100 Subject: [PATCH 740/771] Remove spurious trailing ':' in doc/misc/eglot.texi * doc/misc/eglot.texi (Customizing Eglot): Remove spurious ':'. --- doc/misc/eglot.texi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 286607738b4..64f4c84dfda 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -885,7 +885,7 @@ supported major mode, and how to invoke that server's program. @xref{Setting Up LSP Servers}, for the details. @vindex eglot-strict-mode -@item eglot-strict-mode: +@item eglot-strict-mode This is @code{nil} by default, meaning that Eglot is generally lenient about non-conforming servers. If you need to debug a server, set this to @w{@code{(disallow-non-standard-keys enforce-required-keys)}}. From ccd0ad72f2fb2ccb96f826ecbefe495cb2bffc98 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 19 Oct 2022 21:19:08 +0800 Subject: [PATCH 741/771] Fix build warning without XKB, Xmb, and XInput 2 * src/xterm.c (handle_one_xevent): Avoid defining USE_SAFE_ALLOCA when SAFE_ALLOCA is not actually used. --- src/xterm.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/xterm.c b/src/xterm.c index 3075b5af230..04247bc302d 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -17940,7 +17940,11 @@ handle_one_xevent (struct x_display_info *dpyinfo, GdkDisplay *gdpy = gdk_x11_lookup_xdisplay (dpyinfo->display); #endif int dx, dy; + + /* Avoid warnings when SAFE_ALLOCA is not actually used. */ +#if defined HAVE_XINPUT2 || defined HAVE_XKB || defined HAVE_X_I18N USE_SAFE_ALLOCA; +#endif /* This function is not reentrant, so input should be blocked before it is called. */ @@ -24220,7 +24224,10 @@ handle_one_xevent (struct x_display_info *dpyinfo, count++; } +#if defined HAVE_XINPUT2 || defined HAVE_XKB || defined HAVE_X_I18N SAFE_FREE (); +#endif + return count; } From 6b82958b9fad5fcf784cf93e44dc118cf554d927 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 19 Oct 2022 21:31:54 +0800 Subject: [PATCH 742/771] Fix various builds * src/xterm.c (x_handle_selection_monitor_event): Adjust for build without XInput2. (x_maybe_clear_preedit, xim_destroy_callback): Make conditional on Release 6 XIM. (x_get_keyboard_modifiers): Adjust for build without XCB. --- src/xterm.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/xterm.c b/src/xterm.c index 04247bc302d..ade5600f4da 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -17857,7 +17857,7 @@ x_handle_wm_state (struct frame *f, struct input_event *ie) static bool x_handle_selection_monitor_event (struct x_display_info *dpyinfo, - XEvent *event) + const XEvent *event) { XFixesSelectionNotifyEvent *notify; int i; @@ -25698,6 +25698,14 @@ x_new_font (struct frame *f, Lisp_Object font_object, int fontset) #ifdef HAVE_X11R6 +/* HAVE_X11R6 means Xlib conforms to the R6 specification or later. + HAVE_X11R6_XIM, OTOH, means that Emacs should try to use R6-style + callback driven input method initialization. They are separate + because Sun apparently ships buggy Xlib with some versions of + Solaris... */ + +#ifdef HAVE_X11R6_XIM + /* If preedit text is set on F, cancel preedit, free the text, and generate the appropriate events to cancel the preedit display. @@ -25763,6 +25771,8 @@ xim_destroy_callback (XIM xim, XPointer client_data, XPointer call_data) unblock_input (); } +#endif + #endif /* HAVE_X11R6 */ /* Open the connection to the XIM server on display DPYINFO. @@ -30512,8 +30522,14 @@ x_get_keyboard_modifiers (struct x_display_info *dpyinfo) /* This sometimes happens when the function is called during display initialization, which can happen while obtaining vendor specific keysyms. */ + +#ifdef HAVE_XKB if (!dpyinfo->xkb_desc && !dpyinfo->modmap) x_find_modifier_meanings (dpyinfo); +#else + if (!dpyinfo->modmap) + x_find_modifier_meanings (dpyinfo); +#endif return list5 (make_uint (dpyinfo->hyper_mod_mask), make_uint (dpyinfo->super_mod_mask), From b96f441139e09d48880e1f20ad4237400529ac5f Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Wed, 19 Oct 2022 16:15:02 +0200 Subject: [PATCH 743/771] ; Delete file after wallpaper test * test/lisp/image/wallpaper-tests.el (wallpaper-set/calls-init-action): Don't leave temp file behind. --- test/lisp/image/wallpaper-tests.el | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/lisp/image/wallpaper-tests.el b/test/lisp/image/wallpaper-tests.el index cb6818f8c1b..ab980a11bb6 100644 --- a/test/lisp/image/wallpaper-tests.el +++ b/test/lisp/image/wallpaper-tests.el @@ -99,9 +99,16 @@ ("touch" "touch" fil :init-action (lambda () (setq called t))))) (wallpaper-command (wallpaper--find-command)) - (wallpaper-command-args (wallpaper--find-command-args))) + (wallpaper-command-args (wallpaper--find-command-args)) + (start (time-convert nil 'integer)) + (timeout 3) process) (should (functionp (wallpaper-setter-init-action wallpaper--current-setter))) - (wallpaper-set fil-jpg) + (setq process (wallpaper-set fil-jpg)) + ;; Wait for "touch" process to exit so temp file is removed. + (while (and (< (- (time-convert nil 'integer) start) + timeout) + (process-live-p process)) + (sit-for 0.01)) (should called))))) (ert-deftest wallpaper-set/calls-wallpaper-set-function () From fb8276a17c26b4c2bf8281210ede114ff9e24958 Mon Sep 17 00:00:00 2001 From: Alan Mackenzie Date: Wed, 19 Oct 2022 14:52:14 +0000 Subject: [PATCH 744/771] CC Mode: Cease adding types to found-types too eagerly This fixes bug #58537 and bug #58539. * lisp/progmodes/cc-engine.el (c-forward-type): Remove trailing whitespace from an identifier before passing it to c-add-type. (c-forward-decl-or-cast-1): CASE 3: Do not recognize two consecutive identifiers as type + variable/function unless certain conditions are met. CASE 10: Do not recognize the "type" as a found type unless certain condtions are met. (Near end): Do not recognize the identifier in a cast as a type unless certain conditions are met. * lisp/progmodes/cc-fonts.el (c-get-fontification-context): Recognize being in declaration parens when there is a syntactially wrong "foo ((bar))" preceding the match position. * lisp/progmodes/cc-mode.el (c-update-new-id): Set c-new-id-is-type unconditionally to nil to prevent a second identifier being wrongly marked as a type. --- lisp/progmodes/cc-engine.el | 35 ++++++++++++++++++++++++++++------- lisp/progmodes/cc-fonts.el | 17 +++++++++++++++-- lisp/progmodes/cc-mode.el | 5 +++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/cc-engine.el b/lisp/progmodes/cc-engine.el index 596cccdf48e..e71560fa25f 100644 --- a/lisp/progmodes/cc-engine.el +++ b/lisp/progmodes/cc-engine.el @@ -9106,7 +9106,9 @@ multi-line strings (but not C++, for example)." (when (save-excursion (goto-char post-prefix-pos) (looking-at c-self-contained-typename-key)) - (c-add-type pos (point))) + (c-add-type pos (save-excursion + (c-backward-syntactic-ws) + (point)))) (when (and c-record-type-identifiers c-last-identifier-range) (c-record-type-id c-last-identifier-range))) @@ -9191,7 +9193,10 @@ multi-line strings (but not C++, for example)." (goto-char id-end) (if (or res c-promote-possible-types) (progn - (c-add-type id-start id-end) + (c-add-type id-start (save-excursion + (goto-char id-end) + (c-backward-syntactic-ws) + (point))) (when (and c-record-type-identifiers id-range) (c-record-type-id id-range)) (unless res @@ -10762,8 +10767,16 @@ This function might do hidden buffer changes." (setq backup-if-not-cast t) (throw 'at-decl-or-cast t))) - (setq backup-if-not-cast t) - (throw 'at-decl-or-cast t))) + ;; If we're in declaration or template delimiters, or one + ;; of a certain set of characters follows, we've got a + ;; type and variable. + (if (or (memq context '(decl <>)) + (memq (char-after) '(?\; ?, ?= ?\( ?{ ?:))) + (progn + (setq backup-if-not-cast t) + (throw 'at-decl-or-cast t)) + ;; We're probably just typing a statement. + (throw 'at-decl-or-cast nil)))) ;; CASE 4 (when (and got-suffix @@ -10879,8 +10892,13 @@ This function might do hidden buffer changes." ;; CASE 10 (when at-decl-or-cast - ;; By now we've located the type in the declaration that we know - ;; we're in. + ;; By now we've located the type in the declaration that we think + ;; we're in. Do we have enough evidence to promote the putative + ;; type to a found type? The user may be halfway through typing + ;; a statement beginning with an identifier. + (when (and (eq at-type 'maybe) + (not (eq context 'top))) + (setq c-record-type-identifiers nil)) (throw 'at-decl-or-cast t)) ;; CASE 11 @@ -11123,7 +11141,10 @@ This function might do hidden buffer changes." (not (c-on-identifier))))))))) ;; Handle the cast. - (when (and c-record-type-identifiers at-type (not (eq at-type t))) + (when (and c-record-type-identifiers + at-type + (not (memq at-type '(t maybe)))) ; 'maybe isn't strong enough + ; evidence to promote the type. (let ((c-promote-possible-types t)) (goto-char type-start) (c-forward-type))) diff --git a/lisp/progmodes/cc-fonts.el b/lisp/progmodes/cc-fonts.el index b4ff32b9070..aa16da70703 100644 --- a/lisp/progmodes/cc-fonts.el +++ b/lisp/progmodes/cc-fonts.el @@ -1197,8 +1197,21 @@ casts and declarations are fontified. Used on level 2 and higher." ;; arguments lists (i.e. lists enclosed by <...>) is more strict about what ;; characters it allows within the list. (let ((type (and (> match-pos (point-min)) - (c-get-char-property (1- match-pos) 'c-type)))) - (cond ((not (memq (char-before match-pos) '(?\( ?, ?\[ ?< ?{))) + (c-get-char-property (1- match-pos) 'c-type))) + id-pos) + (cond + ;; Are we just after something like "(foo((bar))" ? + ((and (eq (char-before match-pos) ?\)) + (c-go-list-backward match-pos) + (progn + (c-backward-syntactic-ws) + (and (setq id-pos (c-on-identifier)) + (goto-char id-pos) + (progn + (c-backward-syntactic-ws) + (eq (char-before) ?\())))) + (c-get-fontification-context (point) not-front-decl toplev)) + ((not (memq (char-before match-pos) '(?\( ?, ?\[ ?< ?{))) (cons (and toplev 'top) nil)) ;; A control flow expression or a decltype ((and (eq (char-before match-pos) ?\() diff --git a/lisp/progmodes/cc-mode.el b/lisp/progmodes/cc-mode.el index dce300f33c9..2aa6b90dea3 100644 --- a/lisp/progmodes/cc-mode.el +++ b/lisp/progmodes/cc-mode.el @@ -2080,13 +2080,14 @@ with // and /*, not more generic line and block comments." (defun c-update-new-id (end) ;; Note the bounds of any identifier that END is in or just after, in ;; `c-new-id-start' and `c-new-id-end'. Otherwise set these variables to - ;; nil. + ;; nil. Set `c-new-id-is-type' unconditionally to nil. (save-excursion (goto-char end) (let ((id-beg (c-on-identifier))) (setq c-new-id-start id-beg c-new-id-end (and id-beg - (progn (c-end-of-current-token) (point))))))) + (progn (c-end-of-current-token) (point))) + c-new-id-is-type nil)))) (defun c-post-command () ;; If point was inside of a new identifier and no longer is, record that From a57a3746d8404abef82b6d0de670b2358ef01a24 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Wed, 19 Oct 2022 20:53:36 +0200 Subject: [PATCH 745/771] ; * test/lisp/image/wallpaper-tests.el: Simplify last change. --- test/lisp/image/wallpaper-tests.el | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/lisp/image/wallpaper-tests.el b/test/lisp/image/wallpaper-tests.el index ab980a11bb6..a5d3343bd4d 100644 --- a/test/lisp/image/wallpaper-tests.el +++ b/test/lisp/image/wallpaper-tests.el @@ -100,15 +100,11 @@ :init-action (lambda () (setq called t))))) (wallpaper-command (wallpaper--find-command)) (wallpaper-command-args (wallpaper--find-command-args)) - (start (time-convert nil 'integer)) - (timeout 3) process) + process) (should (functionp (wallpaper-setter-init-action wallpaper--current-setter))) (setq process (wallpaper-set fil-jpg)) ;; Wait for "touch" process to exit so temp file is removed. - (while (and (< (- (time-convert nil 'integer) start) - timeout) - (process-live-p process)) - (sit-for 0.01)) + (accept-process-output process 3) (should called))))) (ert-deftest wallpaper-set/calls-wallpaper-set-function () From 56c63ca21b3e5e2d0bb05d3897ea287a754c5b29 Mon Sep 17 00:00:00 2001 From: Andrea Corallo Date: Wed, 19 Oct 2022 22:08:41 +0200 Subject: [PATCH 746/771] * Fix async native compilation (bug#58637) * lisp/emacs-lisp/comp.el (comp--native-compile): Fix gate condition. (comp-run-async-workers): Add assetion. --- lisp/emacs-lisp/comp.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/emacs-lisp/comp.el b/lisp/emacs-lisp/comp.el index 2c9b79334ba..5a05fe4854b 100644 --- a/lisp/emacs-lisp/comp.el +++ b/lisp/emacs-lisp/comp.el @@ -3928,6 +3928,7 @@ processes from `comp-async-compilations'" "Start compiling files from `comp-files-queue' asynchronously. When compilation is finished, run `native-comp-async-all-done-hook' and display a message." + (cl-assert (null comp-no-spawn)) (if (or comp-files-queue (> (comp-async-runnings) 0)) (unless (>= (comp-async-runnings) (comp-effective-async-max-jobs)) @@ -4048,7 +4049,7 @@ the deferred compilation mechanism." (stringp function-or-file)) (signal 'native-compiler-error (list "Not a function symbol or file" function-or-file))) - (unless comp-no-spawn + (when (or (null comp-no-spawn) comp-async-compilation) (catch 'no-native-compile (let* ((print-symbols-bare t) (data function-or-file) From fa7c5c8707cae227e5b9cb7701ec713a66349083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rudolf=20Adamkovi=C4=8D?= Date: Wed, 19 Oct 2022 20:57:03 +0200 Subject: [PATCH 747/771] Add 'slovak-querty' input method (bug#58642) * lisp/leim/quail/slovak.el ("slovak-querty"): New input method. * etc/NEWS: Mention the new 'slovak-querty' input method. --- etc/NEWS | 5 ++ lisp/leim/quail/slovak.el | 123 +++++++++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 2 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index a378b5acbbc..35d2fefeafd 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1362,6 +1362,11 @@ change the input method's translation rules, customize the user option This supports the keyboard layout specifically designed for the Tamil language. +--- +*** New 'slovak-qwerty' input method +This completes the set of the standard Slovak keyboards, now +containing both the QWERTZ and the QWERTY variants. + * Changes in Specialized Modes and Packages in Emacs 29.1 diff --git a/lisp/leim/quail/slovak.el b/lisp/leim/quail/slovak.el index acde11d02a7..a855ecbf916 100644 --- a/lisp/leim/quail/slovak.el +++ b/lisp/leim/quail/slovak.el @@ -3,7 +3,8 @@ ;; Copyright (C) 1998, 2001-2022 Free Software Foundation, Inc. ;; Authors: Tibor Šimko -;; Milan Zamazal +;; Milan Zamazal +;; Rudolf Adamkovič ;; Maintainer: Pavel Janík ;; Keywords: i18n, multilingual, input method, Slovak @@ -25,8 +26,9 @@ ;;; Commentary: ;; This file defines the following Slovak keyboards: -;; - standard Slovak keyboard +;; - standard Slovak keyboards, QWERTZ and QWERTY variants ;; - three Slovak keyboards for programmers +;; LocalWords: QWERTZ ;;; Code: @@ -154,6 +156,123 @@ ("+0" ?\))) +(quail-define-package + "slovak-querty" "Slovak" "SK" t + "Standard Slovak keyboard, QWERTY variant." + nil t nil nil t nil nil nil nil nil t) + +(quail-define-rules + ("1" ?+) + ("2" ?ľ) + ("3" ?š) + ("4" ?č) + ("5" ?ť) + ("6" ?ž) + ("7" ?ý) + ("8" ?á) + ("9" ?í) + ("0" ?é) + ("!" ?1) + ("@" ?2) + ("#" ?3) + ("$" ?4) + ("%" ?5) + ("^" ?6) + ("&" ?7) + ("*" ?8) + ("(" ?9) + (")" ?0) + ("-" ?=) + ("_" ?%) + ("=" ?') + ("[" ?ú) + ("{" ?/) + ("]" ?ä) + ("}" ?\() + ("\\" ?ň) + ("|" ?\)) + (";" ?ô) + (":" ?\") + ("'" ?§) + ("\"" ?!) + ("<" ??) + (">" ?:) + ("/" ?-) + ("?" ?_) + ("`" ?\;) + ("~" ?^) + ("=a" ?á) + ("+a" ?ä) + ("+=a" ?ä) + ("+c" ?č) + ("+d" ?ď) + ("=e" ?é) + ("+e" ?ě) + ("=i" ?í) + ("=l" ?ĺ) + ("+l" ?ľ) + ("+n" ?ň) + ("=o" ?ó) + ("+o" ?ô) + ("~o" ?ô) + ("+=o" ?ö) + ("=r" ?ŕ) + ("+r" ?ř) + ("=s" ?ß) + ("+s" ?š) + ("+t" ?ť) + ("=u" ?ú) + ("+u" ?ů) + ("+=u" ?ü) + ("=y" ?ý) + ("+z" ?ž) + ("=A" ?Á) + ("+A" ?Ä) + ("+=A" ?Ä) + ("+C" ?Č) + ("+D" ?Ď) + ("=E" ?É) + ("+E" ?Ě) + ("=I" ?Í) + ("=L" ?Ĺ) + ("+L" ?Ľ) + ("+N" ?Ň) + ("=O" ?Ó) + ("+O" ?Ô) + ("~O" ?Ô) + ("+=O" ?Ö) + ("=R" ?Ŕ) + ("+R" ?Ř) + ("=S" ?ß) + ("+S" ?Š) + ("+T" ?Ť) + ("=U" ?Ú) + ("+U" ?Ů) + ("+=U" ?Ü) + ("=Y" ?Ý) + ("+Z" ?Ž) + ("=q" ?`) + ("=2" ?@) + ("=3" ?#) + ("=4" ?$) + ("=5" ?%) + ("=6" ?^) + ("=7" ?&) + ("=8" ?*) + ("=9" ?\() + ("=0" ?\)) + ("+1" ?!) + ("+2" ?@) + ("+3" ?#) + ("+4" ?$) + ("+5" ?%) + ("+6" ?^) + ("+7" ?&) + ("+8" ?*) + ("+9" ?\() + ("+0" ?\))) + + (quail-define-package "slovak-prog-1" "Slovak" "SK" t "Slovak (non-standard) keyboard for programmers #1. From 663fad561d9a18d9b1291f63fe9e9ac1062cf9aa Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Thu, 20 Oct 2022 09:58:18 +0300 Subject: [PATCH 748/771] ; Fix documentation and name of 'slovak-qwerty' input method * etc/NEWS: Fix entry for slovak-qwerty. * lisp/leim/quail/slovak.el ("slovak", "slovak-qwerty"): Doc fix. --- etc/NEWS | 6 +++--- lisp/leim/quail/slovak.el | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index 35d2fefeafd..be90d1beef5 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1363,9 +1363,9 @@ This supports the keyboard layout specifically designed for the Tamil language. --- -*** New 'slovak-qwerty' input method -This completes the set of the standard Slovak keyboards, now -containing both the QWERTZ and the QWERTY variants. +*** New input method 'slovak-qwerty'. +This is a variant of the 'slovak' input method, which corresponds to +the QWERTY Slovak keyboards. * Changes in Specialized Modes and Packages in Emacs 29.1 diff --git a/lisp/leim/quail/slovak.el b/lisp/leim/quail/slovak.el index a855ecbf916..8ddd92d5b4d 100644 --- a/lisp/leim/quail/slovak.el +++ b/lisp/leim/quail/slovak.el @@ -37,7 +37,7 @@ (quail-define-package "slovak" "Slovak" "SK" t - "Standard Slovak keyboard." + "Standard Slovak QWERTZ keyboard." nil t nil nil t nil nil nil nil nil t) (quail-define-rules @@ -157,8 +157,8 @@ (quail-define-package - "slovak-querty" "Slovak" "SK" t - "Standard Slovak keyboard, QWERTY variant." + "slovak-qwerty" "Slovak" "SK" t + "Standard Slovak QWERTY keyboard." nil t nil nil t nil nil nil nil nil t) (quail-define-rules From 25cf39162e0a78406409842b96164c813eb8c337 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 20 Oct 2022 09:32:58 +0200 Subject: [PATCH 749/771] Prefer defvar-keymap in modula2.el * lisp/progmodes/modula2.el (m2-mode-map): Prefer defvar-keymap. --- lisp/progmodes/modula2.el | 63 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/lisp/progmodes/modula2.el b/lisp/progmodes/modula2.el index e668570ba17..09cb848fd52 100644 --- a/lisp/progmodes/modula2.el +++ b/lisp/progmodes/modula2.el @@ -65,39 +65,36 @@ "Column for aligning the end of a comment, in Modula-2." :type 'integer) -;;; Added by TEP -(defvar m2-mode-map - (let ((map (make-sparse-keymap))) - ;; FIXME: Many of those bindings are contrary to coding conventions. - (define-key map "\C-cb" #'m2-begin) - (define-key map "\C-cc" #'m2-case) - (define-key map "\C-cd" #'m2-definition) - (define-key map "\C-ce" #'m2-else) - (define-key map "\C-cf" #'m2-for) - (define-key map "\C-ch" #'m2-header) - (define-key map "\C-ci" #'m2-if) - (define-key map "\C-cm" #'m2-module) - (define-key map "\C-cl" #'m2-loop) - (define-key map "\C-co" #'m2-or) - (define-key map "\C-cp" #'m2-procedure) - (define-key map "\C-c\C-w" #'m2-with) - (define-key map "\C-cr" #'m2-record) - (define-key map "\C-cs" #'m2-stdio) - (define-key map "\C-ct" #'m2-type) - (define-key map "\C-cu" #'m2-until) - (define-key map "\C-cv" #'m2-var) - (define-key map "\C-cw" #'m2-while) - (define-key map "\C-cx" #'m2-export) - (define-key map "\C-cy" #'m2-import) - (define-key map "\C-c{" #'m2-begin-comment) - (define-key map "\C-c}" #'m2-end-comment) - (define-key map "\C-c\C-z" #'suspend-emacs) - (define-key map "\C-c\C-v" #'m2-visit) - (define-key map "\C-c\C-t" #'m2-toggle) - (define-key map "\C-c\C-l" #'m2-link) - (define-key map "\C-c\C-c" #'m2-compile) - map) - "Keymap used in Modula-2 mode.") +(defvar-keymap m2-mode-map + :doc "Keymap used in Modula-2 mode." + ;; FIXME: Many of those bindings are contrary to coding conventions. + "C-c b" #'m2-begin + "C-c c" #'m2-case + "C-c d" #'m2-definition + "C-c e" #'m2-else + "C-c f" #'m2-for + "C-c h" #'m2-header + "C-c i" #'m2-if + "C-c m" #'m2-module + "C-c l" #'m2-loop + "C-c o" #'m2-or + "C-c p" #'m2-procedure + "C-c C-w" #'m2-with + "C-c r" #'m2-record + "C-c s" #'m2-stdio + "C-c t" #'m2-type + "C-c u" #'m2-until + "C-c v" #'m2-var + "C-c w" #'m2-while + "C-c x" #'m2-export + "C-c y" #'m2-import + "C-c {" #'m2-begin-comment + "C-c }" #'m2-end-comment + "C-c C-z" #'suspend-emacs + "C-c C-v" #'m2-visit + "C-c C-t" #'m2-toggle + "C-c C-l" #'m2-link + "C-c C-c" #'m2-compile) (defcustom m2-indent 5 "This variable gives the indentation in Modula-2 mode." From 0c7024d0d9172322052de2ee571ba64afff905f0 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 20 Oct 2022 11:02:09 +0200 Subject: [PATCH 750/771] * doc/misc/Makefile.in (INFO_COMMON): Add eglot. --- doc/misc/Makefile.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/misc/Makefile.in b/doc/misc/Makefile.in index 1d881a5fc7f..b6eef7ea799 100644 --- a/doc/misc/Makefile.in +++ b/doc/misc/Makefile.in @@ -68,7 +68,7 @@ DOCMISC_W32 = @DOCMISC_W32@ ## Info files to build and install on all platforms. INFO_COMMON = auth autotype bovine calc ccmode cl \ - dbus dired-x ebrowse ede ediff edt eieio \ + dbus dired-x ebrowse ede ediff edt eglot eieio \ emacs-mime epa erc ert eshell eudc efaq eww \ flymake forms gnus emacs-gnutls htmlfontify idlwave ido info.info \ mairix-el message mh-e modus-themes newsticker nxml-mode octave-mode \ From 4725c123f33eb9579b695748fa9f85c9af3eb01a Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 20 Oct 2022 11:38:18 +0200 Subject: [PATCH 751/771] ; eglot.texi: Fix typos and minor inconsistencies * doc/misc/eglot.texi: Fix typos and minor inconsistencies. --- doc/misc/eglot.texi | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 64f4c84dfda..bd92582f900 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -89,7 +89,7 @@ This manual documents how to configure, use, and customize Eglot. @menu * Quick Start:: For the impatient. -* Eglot and LSP Servers:: How to work with language servers +* Eglot and LSP Servers:: How to work with language servers. * Using Eglot:: Important Eglot commands and variables. * Customizing Eglot:: Eglot customization and advanced features. * Troubleshooting Eglot:: Troubleshooting and reporting bugs. @@ -201,13 +201,13 @@ The value of the variable is an alist, whose elements are of the form The @var{major-mode} of the alist elements can be either a symbol of an Emacs major mode or a list of the form @w{@code{(@var{mode} :language-id @var{id})}}, with @var{mode} being a major-mode symbol -and @var{id} a string that identifies the language to the server (if -Eglot cannot by itself convert the major-mode to the language -identifier string required by the server). In addition, -@var{major-mode} can be a list of several major mode specified in one -of the above forms -- this means a running instance of the associated -server is responsible for files of multiple major modes or languages -in the project. +and @var{id} a string that identifies the language to the server. The +latter form should be used if Eglot cannot by itself convert the +major-mode to the language identifier string required by the server. +In addition, @var{major-mode} can be a list of several major modes +specified in one of the above forms -- this means a running instance +of the associated server is responsible for files of multiple major +modes or languages in the project. The @var{server} part of the alist elements can be one of the following: @@ -327,12 +327,12 @@ Emacs session, it runs the hook @code{eglot-connect-hook} When Eglot is turned on, it arranges for turning itself off automatically if the language server process terminates. Turning off -Eglot means it shuts down the server connection, ceases its management -of all the buffers that use the server connection which was +Eglot means that it shuts down the server connection, ceases its +management of all the buffers that use the server connection which was terminated, deactivates its minor mode, and restores the original values of the Emacs variables that Eglot changed when it was turned -on. @xref{Eglot and Buffers}, for more details of what does Eglot -management of a buffer entail. +on. @xref{Eglot and Buffers}, for more details of what Eglot +management of a buffer entails. @findex eglot-shutdown You can also shut down a language server manually, by using the @@ -372,7 +372,7 @@ commands and variables. Once Eglot is enabled in a buffer, it uses LSP and the language-server capabilities to activate, enable, and enhance modern IDE features in Emacs. The features themselves are usually provided via other Emacs -packages. Here are the main features Eglot enables and provides: +packages. These are the main features that Eglot enables and provides: @itemize @bullet @item @@ -395,7 +395,7 @@ emacs, GNU Emacs Manual}). Eglot provides a backend for the Xref capabilities which uses the language-server understanding of the program source. In particular, it eliminates the need to generate tags tables (@pxref{Tags tables,,, emacs, GNU Emacs Manual}) for -languages which are only supported by the @code{etags} backend. +languages that are only supported by the @code{etags} backend. @item Buffer navigation by name of function, class, method, etc., via Imenu @@ -416,7 +416,7 @@ activated automatically as you type. @item If a completion package such as @code{company-mode}, a popular -3rd-party completion package, is installed, Eglot enhances it by +third-party completion package, is installed, Eglot enhances it by providing completion candidates based on the language-server analysis of the source code. @@ -505,7 +505,7 @@ directory. @item A VC project: source files in a directory hierarchy under some VCS, -i.e.@: a VCS repository (@pxref{Version Control,,, emacs, GNU Emacs +e.g.@: a Git repository (@pxref{Version Control,,, emacs, GNU Emacs Manual}). @item @@ -618,8 +618,9 @@ will be added to those managed by an existing server session. The command attempts to figure out the buffer's major mode and the suitable language server; in case it fails, it might prompt for the major mode to use and for the server program to start. If invoked -with @kbd{C-u}, it always prompts for the server program, and if -invoked with @kbd{C-u C-u}, also prompt for the major mode. +with a prefix argument @kbd{C-u}, it always prompts for the server +program, and if invoked with @kbd{C-u C-u}, also prompt for the major +mode. If the language server is successfully started and contacted, this command arranges for any other buffers belonging to the same project @@ -640,7 +641,7 @@ can sometimes be useful to unclog a partially malfunctioning server connection. @item M-x eglot-shutdown -Shut down a language server. This commands prompts for a language +Shuts down a language server. This commands prompts for a language server to shut down (unless there's only one server session, and it manages the current buffer). Then the command shuts down the server and stops managing the buffers the server was used for. Emacs @@ -863,7 +864,7 @@ customize the Flymake faces @code{flymake-error} and @item To configure the amount of space taken up by documentation in the -echo area, the customize the ElDoc variable +echo area, customize the ElDoc variable @code{eldoc-echo-area-use-multiline-p}. @item @@ -1008,7 +1009,7 @@ Alternatively, the same configuration could be defined as follows: This is an equivalent setup which sets the value for all the major-modes inside the project; Eglot will use for each server only -the section of the parameters intended for that server +the section of the parameters intended for that server. As yet another alternative, you can set the value of @code{eglot-workspace-configuration} programmatically, via the From 2d2cdb4741a3c1e42c8ed771303a878fd428911b Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 20 Oct 2022 11:40:37 +0200 Subject: [PATCH 752/771] eglot.texi: Move sentence on LSP Servers earlier * doc/misc/eglot.texi (Setting Up LSP Servers): Move explanation on the (lack of) need for customizing servers earlier. --- doc/misc/eglot.texi | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index bd92582f900..9ed482cec62 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -185,8 +185,10 @@ further discussed in this manual; refer to the documentation of the particular server(s) you want to install. To use a language server, Eglot must know how to start it and which -programming languages each server supports. This information is -provided by the @code{eglot-server-programs} variable. +programming languages each server supports. Eglot comes with a fairly +complete set of associations of major-modes to popular language +servers predefined. This information is provided by the +@code{eglot-server-programs} variable. @defvar eglot-server-programs This variable associates major modes with names and command-line @@ -249,9 +251,7 @@ arguments. @end defvar -Eglot comes with a fairly complete set of associations of major-modes -to popular language servers predefined in this variable. If you need -to add server associations to the default list, use +If you need to add server associations to the default list, use @code{add-to-list}. For example, if there is a hypothetical language server program @command{fools} for the language @code{Foo} which is supported by an Emacs major-mode @code{foo-mode}, you can add it to From 16986a9cc42ef4de580456f4acc5feba682ac8b1 Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 20 Oct 2022 11:42:29 +0200 Subject: [PATCH 753/771] eglot.texi: Make example more realistic * doc/misc/eglot.texi (Eglot and Buffers): Prefer more realistic *.c instead of *.foo in example. --- doc/misc/eglot.texi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 9ed482cec62..e5f26060827 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -523,13 +523,13 @@ directory, which is usually the top-level directory of the project's directory hierarchy. This ensures the language server has the same comprehensive view of the project's files as you do. -For example, if you visit the file @file{~/projects/fooey/lib/x.foo} -and @file{x.foo} belongs to a project rooted at +For example, if you visit the file @file{~/projects/fooey/lib/x.c} +and @file{x.c} belongs to a project rooted at @file{~/projects/fooey} (perhaps because a @file{.git} directory exists there), then @kbd{M-x eglot} causes the server program to start with that root as the current working directory. The server then will -analyze not only the file @file{lib/x.foo} you visited, but likely -also all the other @file{*.foo} files under the +analyze not only the file @file{lib/x.c} you visited, but likely +also all the other @file{*.c} files under the @file{~/projects/fooey} directory. In some cases, additional information specific to a given project will @@ -592,8 +592,8 @@ When you visit a file under the same project, whether an existing or a new file, its buffer is automatically added to the set of buffers managed by Eglot, and the server which supports the buffer's major-mode is notified about that. Thus, visiting a non-existent file -@file{/home/joe/projects/fooey/lib/y.foo} in the above example will -notify the server of the @file{*.foo} files' language that a new file +@file{/home/joe/projects/fooey/lib/y.c} in the above example will +notify the server of the @file{*.c} files' language that a new file was added to the project, even before the file appears on disk. The special Eglot minor mode is also turned on automatically in the buffer visiting the file. From 5d73bc5c69f56ce1b22c950d19ef6406b662949f Mon Sep 17 00:00:00 2001 From: Stefan Kangas Date: Thu, 20 Oct 2022 11:43:08 +0200 Subject: [PATCH 754/771] eglot.texi: Explain where to find third-party packages * doc/misc/eglot.texi (Eglot Features): Improve description on third-party packages. --- doc/misc/eglot.texi | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index e5f26060827..caf09769b49 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -418,19 +418,21 @@ activated automatically as you type. If a completion package such as @code{company-mode}, a popular third-party completion package, is installed, Eglot enhances it by providing completion candidates based on the language-server analysis -of the source code. +of the source code. (@code{company-mode} can be installed from GNU ELPA.) @item -If @code{yasnippet}, a popular package for automatic insertion of code -templates (snippets), is installed, and the language server supports -snippet completion candidates, Eglot arranges for the completion -package to instantiate these snippets using @code{yasnippet}. +If @code{yasnippet}, a popular third-party package for automatic +insertion of code templates (snippets), is installed, and the language +server supports snippet completion candidates, Eglot arranges for the +completion package to instantiate these snippets using +@code{yasnippet}. (@code{yasnippet} can be installed from GNU ELPA.) @item -If the popular package @code{markdown-mode} is installed, and the -server provides at-point documentation formatted as Markdown in -addition to plain text, Eglot arranges for the ElDoc package to enrich -this text with e.g. fontification before displaying it to the user. +If the popular third-party package @code{markdown-mode} is installed, +and the server provides at-point documentation formatted as Markdown +in addition to plain text, Eglot arranges for the ElDoc package to +enrich this text with e.g. fontification before displaying it to the +user. @item In addition to enabling and enhancing other features and packages, From eb9d6281b58f50927afdc2fdb2fcebf76e2ffe23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 10 Oct 2022 13:57:26 +0100 Subject: [PATCH 755/771] Do use eglot-connect-timeout if eglot-sync-connect is t Reported by Eli Zaretskii * eglot.el (eglot--connect): Use eglot-connect-timeout in the case eglot-sync-connect is t. --- lisp/progmodes/eglot.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index d3f5935a9ef..18523067fa0 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1282,7 +1282,8 @@ in project `%s'." (cond ((numberp eglot-sync-connect) (accept-process-output nil eglot-sync-connect)) (eglot-sync-connect - (while t (accept-process-output nil 30))))))) + (while t (accept-process-output + nil eglot-connect-timeout))))))) (pcase retval (`(error . ,msg) (eglot--error msg)) (`nil (eglot--message "Waiting in background for server `%s'" From 9801e217f9842190f2303e46f6d41202cfe6b546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 20 Oct 2022 10:48:11 +0100 Subject: [PATCH 756/771] Rework header of eglot.el * eglot.el (Commentary): Rework. --- lisp/progmodes/eglot.el | 85 ++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 18523067fa0..1520d991ffa 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1,4 +1,4 @@ -;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- +;;; eglot.el --- The Emacs Client for LSP servers -*- lexical-binding: t; -*- ;; Copyright (C) 2018-2022 Free Software Foundation, Inc. @@ -7,11 +7,11 @@ ;; Maintainer: João Távora ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages -;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0") (seq "2.23")) +;; Package-Requires: ((emacs "26.3") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0") (seq "2.23")) -;; This is (or will soon) be a GNU ELPA :core package. Avoid using -;; functionality that not compatible with the version of Emacs -;; recorded above. +;; This is is a GNU ELPA :core package. Avoid adding functionality +;; that is not available in the version of Emacs recorded above or any +;; of the package dependencies. ;; This file is part of GNU Emacs. @@ -33,32 +33,63 @@ ;; Eglot ("Emacs Polyglot") is an Emacs LSP client that stays out of ;; your way. ;; -;; Typing M-x eglot should be enough to get you started, but here's a -;; little info (see the accompanying README.md or the URL for more). +;; Typing M-x eglot in some source file is often enough to get you +;; started, if the language server you're looking to use is installed +;; in your system. Please refer to the manual, available from +;; https://joaotavora.github.io/eglot/ or from M-x info for more usage +;; instructions. ;; -;; M-x eglot starts a server via a shell command guessed from -;; `eglot-server-programs', using the current major mode (for whatever -;; language you're programming in) as a hint. If it can't guess, it -;; prompts you in the minibuffer for these things. Actually, the -;; server does not need to be running locally: you can connect to a -;; running server via TCP by entering a syntax. +;; If you wish to contribute changes to Eglot, please do read the user +;; manual first. Additionally, take the following in consideration: + +;; * Eglot's main job is to hook up the information that language +;; servers offer via LSP to Emacs's UI facilities: Xref for +;; definition-chasing, Flymake for diagnostics, Eldoc for at-point +;; documentation, etc. Eglot's job is generally *not* to provide +;; such a UI itself, though a small number of simple +;; counter-examples do exist, for example in the `eglot-rename' +;; command. When a new UI is evidently needed, consider adding a +;; new package to Emacs, or extending an existing one. ;; -;; If the connection is successful, you should see an `eglot' -;; indicator pop up in your mode-line. More importantly, this means -;; that current *and future* file buffers of that major mode *inside -;; your current project* automatically become \"managed\" by the LSP -;; server. In other words, information about their content is -;; exchanged periodically to provide enhanced code analysis using -;; `xref-find-definitions', `flymake-mode', `eldoc-mode', -;; `completion-at-point', among others. +;; * Eglot was designed to function with just the UI facilities found +;; in the latest Emacs core, as long as those facilities are also +;; available as GNU ELPA :core packages. Historically, a number of +;; :core packages were added or reworked in Emacs to make this +;; possible. This principle should be upheld when adding new LSP +;; features or tweaking exising ones. Design any new facilities in +;; a way that they could work in the absence of LSP or using some +;; different protocol, then make sure Eglot can link up LSP +;; information to it. + +;; * There are few Eglot configuration variables. This principle +;; should also be upheld. If Eglot had these variables, it could be +;; duplicating configuration found elsewhere, bloating itself up, +;; and making it generally hard to integrate with the ever growing +;; set of LSP features and Emacs packages. For instance, this is +;; why one finds a single variable +;; `eglot-ignored-server-capabilities' instead of a number of +;; capability-specific flags, or why customizing the display of +;; LSP-provided documentation is done via ElDoc's variables, not +;; Eglot's. ;; -;; To "unmanage" these buffers, shutdown the server with -;; M-x eglot-shutdown. +;; * Linking up LSP information to other libraries is generally done +;; in the `eglot--managed-mode' minor mode function, by +;; buffer-locally setting the other library's variables to +;; Eglot-specific versions. When deciding what to set the variable +;; to, the general idea is to choose a good default for beginners +;; that doesn't clash with Emacs's defaults. The settings are only +;; in place during Eglot's LSP-enriched tenure over a project. Even +;; so, some of those decisions will invariably aggravate a minority +;; of Emacs power users, but these users can use `eglot-stay-out-of' +;; and `eglot-managed-mode-hook' to quench their OCD. ;; -;; To start an eglot session automatically when a foo-mode buffer is -;; visited, you can put this in your init file: -;; -;; (add-hook 'foo-mode-hook 'eglot-ensure) +;; * On occasion, to enable new features, Eglot can have soft +;; dependencies on popular libraries that are not in Emacs core. +;; "Soft" means that the dependency doesn't impair any other use of +;; Eglot beyond that feature. Such is the case of the snippet +;; functionality, via the Yasnippet package, Markdown formatting of +;; at-point documentation via the markdown-mode package, and nicer +;; looking completions when the Company package is used. ;;; Code: From 806734c1b1f433de43d59d9a5e3a1e89d64315f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 20 Oct 2022 11:06:44 +0100 Subject: [PATCH 757/771] Expose eglot-{} to be used in eglot-workspace-configuration * eglot.el (eglot-{}): New variable alias. GitHub-reference: per https://github.com/joaotavora/eglot/issues/1084 --- lisp/progmodes/eglot.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 1520d991ffa..901bf30d4bd 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -407,6 +407,7 @@ This can be useful when using docker to run a language server.") (2 . eglot-diagnostic-tag-deprecated-face))) (defconst eglot--{} (make-hash-table :size 1) "The empty JSON object.") +(defvaralias 'eglot-{} 'eglot--{}) (defun eglot--executable-find (command &optional remote) "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26." From 7ee5b0f85f32dd6fd874a12a033798ac56ecbacc Mon Sep 17 00:00:00 2001 From: Mauro Aranda Date: Thu, 20 Oct 2022 07:34:38 -0300 Subject: [PATCH 758/771] Improve HERE document detection in perl-mode * lisp/progmodes/perl-mode.el (perl-syntax-propertize-function): Detect indented HERE documents when using a bare identifier. (perl--syntax-exp-intro-keywords): Recognize HERE documents that come after die, warn and eval. (perl--syntax-exp-intro-regexp): Identify HERE documents when printing to a filehandle with printf? and when they appear after a fat comma. * test/lisp/progmodes/cperl-mode-resources/here-docs.pl: Add more tests. --- lisp/progmodes/perl-mode.el | 9 ++- .../cperl-mode-resources/here-docs.pl | 66 +++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/lisp/progmodes/perl-mode.el b/lisp/progmodes/perl-mode.el index 7b7a2cdf019..c5d5d703fc9 100644 --- a/lisp/progmodes/perl-mode.el +++ b/lisp/progmodes/perl-mode.el @@ -215,11 +215,16 @@ (eval-and-compile (defconst perl--syntax-exp-intro-keywords '("split" "if" "unless" "until" "while" "print" "printf" - "grep" "map" "not" "or" "and" "for" "foreach" "return")) + "grep" "map" "not" "or" "and" "for" "foreach" "return" "die" + "warn" "eval")) (defconst perl--syntax-exp-intro-regexp (concat "\\(?:\\(?:^\\|[^$@&%[:word:]]\\)" (regexp-opt perl--syntax-exp-intro-keywords) + ;; A HERE document as an argument to printf? + ;; when printing to a filehandle. + "\\|printf?[ \t]*$?[_[:alpha:]][_[:alnum:]]*" + "\\|=>" "\\|[?:.,;|&*=!~({[]" "\\|[^-+][-+]" ;Bug#42168: `+' is intro but `++' isn't! "\\|\\(^\\)\\)[ \t\n]*"))) @@ -335,7 +340,7 @@ "<<\\(~\\)?[ \t]*\\('[^'\n]*'\\|\"[^\"\n]*\"\\|\\\\[[:alpha:]][[:alnum:]]*\\)" ;; The < <<~HERE +look-here +HERE + ); + +$noindent = "New statement in this line"; + +=head2 Test case 10 + +A HERE document as an argument to die. + +=cut + +1 or die < Date: Thu, 20 Oct 2022 11:20:30 +0100 Subject: [PATCH 759/771] Fix Eglot manual's description of eglot-workspace-configuration * doc/misc/eglot.texi (Customizing Eglot) (eglot-workspace-configuration): Explain that plist may be arbitrarily complex and correctly identify nil as the Elisp equivalent to JSON null. --- doc/misc/eglot.texi | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index caf09769b49..a05e7fd7ee8 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -964,11 +964,12 @@ this variable should be a property list of the following format: @noindent Here @code{:@var{server}} identifies a particular language server and @var{plist} is the corresponding keyword-value property list of one or -more parameter settings for that server. That list of parameters is -serialized to JSON by Eglot and sent to the server. For that reason -JSON values @code{true}, @code{false}, and @code{@{@}} should be -represented in the property lists as Lisp symbols @code{t}, -@code{:json-false}, and @code{nil}, respectively. +more parameter settings for that server, serialized by Eglot as a JSON +object. @var{plist} may be arbitrarity complex, generally containing +other keywork-value property sublists corresponding to JSON subobjects. +The JSON values @code{true}, @code{false}, @code{null} and @code{@{@}} +are represented by the Lisp values @code{t}, @code{:json-false}, +@code{nil}, and @{eglot-@{@}}, respectively. @findex eglot-show-workspace-configuration When experimenting with workspace settings, you can use the command From 0e7361a5ccce084613d54d6ba3954ffca6074817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 20 Oct 2022 11:42:25 +0100 Subject: [PATCH 760/771] Revert "eglot.texi: Make example more realistic" This quest for realism ignores the fact that a previous example for a hypothetical language Foo and a language server "fools" already exists. It also undermines the intended generality of the instructions. This reverts commit 16986a9cc42ef4de580456f4acc5feba682ac8b1. --- doc/misc/eglot.texi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index a05e7fd7ee8..c1de1e818dc 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -525,13 +525,13 @@ directory, which is usually the top-level directory of the project's directory hierarchy. This ensures the language server has the same comprehensive view of the project's files as you do. -For example, if you visit the file @file{~/projects/fooey/lib/x.c} -and @file{x.c} belongs to a project rooted at +For example, if you visit the file @file{~/projects/fooey/lib/x.foo} +and @file{x.foo} belongs to a project rooted at @file{~/projects/fooey} (perhaps because a @file{.git} directory exists there), then @kbd{M-x eglot} causes the server program to start with that root as the current working directory. The server then will -analyze not only the file @file{lib/x.c} you visited, but likely -also all the other @file{*.c} files under the +analyze not only the file @file{lib/x.foo} you visited, but likely +also all the other @file{*.foo} files under the @file{~/projects/fooey} directory. In some cases, additional information specific to a given project will @@ -594,8 +594,8 @@ When you visit a file under the same project, whether an existing or a new file, its buffer is automatically added to the set of buffers managed by Eglot, and the server which supports the buffer's major-mode is notified about that. Thus, visiting a non-existent file -@file{/home/joe/projects/fooey/lib/y.c} in the above example will -notify the server of the @file{*.c} files' language that a new file +@file{/home/joe/projects/fooey/lib/y.foo} in the above example will +notify the server of the @file{*.foo} files' language that a new file was added to the project, even before the file appears on disk. The special Eglot minor mode is also turned on automatically in the buffer visiting the file. From e0616f2d3cbc559560bf15346dd8a1824603bd80 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 7 Aug 2022 13:49:59 +0800 Subject: [PATCH 761/771] * etc/PROBLEMS: Document window manager focus problems. --- etc/PROBLEMS | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/etc/PROBLEMS b/etc/PROBLEMS index ed2bc1ae051..aaecc41f6e8 100644 --- a/etc/PROBLEMS +++ b/etc/PROBLEMS @@ -1229,6 +1229,17 @@ you should use an Emacs input method instead. ** X keyboard problems +*** `x-focus-frame' fails to activate the frame. + +Some window managers prevent `x-focus-frame' from activating the given +frame when Emacs is in the background. + +Emacs tries to work around this problem by default, but the workaround +does not work on all window managers. You can try different +workarounds by changing the value of `x-allow-focus-stealing' (see its +doc string for more details). The value `imitate-pager' may be +required on some versions of KWin. + *** You "lose characters" after typing Compose Character key. This is because the Compose Character key is defined as the keysym From 6f3ade1c08c6cbf56c0dc0d12e9508c261eb42bf Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 7 Aug 2022 13:46:52 +0800 Subject: [PATCH 762/771] Work around problems setting input focus when a frame is in the background * src/xterm.c (server_timestamp_predicate, x_get_server_time): New functions. (x_ewmh_activate_frame, x_focus_frame, syms_of_xterm): Apply various workarounds for window manager "focus stealing prevention". (bug#57012) --- src/xterm.c | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/src/xterm.c b/src/xterm.c index ade5600f4da..8b3d6f77a6c 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -27134,6 +27134,64 @@ xembed_request_focus (struct frame *f) XEMBED_REQUEST_FOCUS, 0, 0, 0); } +static Bool +server_timestamp_predicate (Display *display, XEvent *xevent, + XPointer arg) +{ + XID *args = (XID *) arg; + + if (xevent->type == PropertyNotify + && xevent->xproperty.window == args[0] + && xevent->xproperty.atom == args[1]) + return True; + + return False; +} + +/* Get the server time. The X server is guaranteed to deliver the + PropertyNotify event, so there is no reason to use x_if_event. */ + +static Time +x_get_server_time (struct frame *f) +{ + Atom property_atom; + XEvent property_dummy; + struct x_display_info *dpyinfo; + XID client_data[2]; +#if defined HAVE_XSYNC && !defined USE_GTK && defined HAVE_CLOCK_GETTIME + uint_fast64_t current_monotonic_time; +#endif + + /* If the server time is the same as the monotonic time, avoid a + roundtrip by using that instead. */ + +#if defined HAVE_XSYNC && !defined USE_GTK && defined HAVE_CLOCK_GETTIME + if (FRAME_DISPLAY_INFO (f)->server_time_monotonic_p) + { + current_monotonic_time = x_sync_current_monotonic_time (); + + if (current_monotonic_time) + /* Truncate the time to CARD32. */ + return (current_monotonic_time / 1000) & X_ULONG_MAX; + } +#endif + + dpyinfo = FRAME_DISPLAY_INFO (f); + property_atom = dpyinfo->Xatom_EMACS_SERVER_TIME_PROP; + client_data[0] = FRAME_OUTER_WINDOW (f); + client_data[1] = property_atom; + + XChangeProperty (dpyinfo->display, FRAME_OUTER_WINDOW (f), + property_atom, XA_ATOM, 32, + PropModeReplace, + (unsigned char *) &property_atom, 1); + + XIfEvent (dpyinfo->display, &property_dummy, + server_timestamp_predicate, (XPointer) client_data); + + return property_dummy.xproperty.time; +} + /* Activate frame with Extended Window Manager Hints */ static void @@ -27141,6 +27199,7 @@ x_ewmh_activate_frame (struct frame *f) { XEvent msg; struct x_display_info *dpyinfo; + Time time; dpyinfo = FRAME_DISPLAY_INFO (f); @@ -27161,6 +27220,43 @@ x_ewmh_activate_frame (struct frame *f) msg.xclient.data.l[3] = 0; msg.xclient.data.l[4] = 0; + /* No frame is currently focused on that display, so apply any + bypass for focus stealing prevention that the user has + specified. */ + if (!dpyinfo->x_focus_frame) + { + if (EQ (Vx_allow_focus_stealing, Qimitate_pager)) + msg.xclient.data.l[0] = 2; + else if (EQ (Vx_allow_focus_stealing, Qnewer_time)) + { + block_input (); + time = x_get_server_time (f); +#ifdef USE_GTK + x_set_gtk_user_time (f, time); +#endif + /* Temporarily override dpyinfo->x_focus_frame so the + user time property is set on the right window. */ + dpyinfo->x_focus_frame = f; + x_display_set_last_user_time (dpyinfo, time, true, true); + dpyinfo->x_focus_frame = NULL; + unblock_input (); + + msg.xclient.data.l[1] = time; + } + else if (EQ (Vx_allow_focus_stealing, Qraise_and_focus)) + { + time = x_get_server_time (f); + + x_ignore_errors_for_next_request (dpyinfo); + XSetInputFocus (FRAME_X_DISPLAY (f), FRAME_OUTER_WINDOW (f), + RevertToParent, time); + XRaiseWindow (FRAME_X_DISPLAY (f), FRAME_OUTER_WINDOW (f)); + x_stop_ignoring_errors (dpyinfo); + + return; + } + } + XSendEvent (dpyinfo->display, dpyinfo->root_window, False, (SubstructureRedirectMask | SubstructureNotifyMask), &msg); @@ -30649,6 +30745,9 @@ With MS Windows, Haiku windowing or Nextstep, the value is t. */); Fput (Qsuper, Qmodifier_value, make_fixnum (super_modifier)); DEFSYM (QXdndSelection, "XdndSelection"); DEFSYM (Qx_selection_alias_alist, "x-selection-alias-alist"); + DEFSYM (Qimitate_pager, "imitate-pager"); + DEFSYM (Qnewer_time, "newer-time"); + DEFSYM (Qraise_and_focus, "raise-and-focus"); DEFVAR_LISP ("x-ctrl-keysym", Vx_ctrl_keysym, doc: /* Which keys Emacs uses for the ctrl modifier. @@ -30902,4 +31001,24 @@ connection setup. */); /* The default value of this variable is chosen so that updating the tool bar does not require a call to _XReply. */ Vx_fast_selection_list = list1 (QCLIPBOARD); + + DEFVAR_LISP ("x-allow-focus-stealing", Vx_allow_focus_stealing, + doc: /* How to bypass window manager focus stealing prevention. + +Some window managers prevent `x-focus-frame' from activating the given +frame when Emacs is in the background, which is especially prone to +cause problems when the Emacs server wants to activate itself. This +variable specifies the strategy used to activate frames when that is +the case, and has several valid values (any other value means to not +bypass window manager focus stealing prevention): + + - The symbol `imitate-pager', which means to pretend that Emacs is a + pager. + + - The symbol `newer-time', which means to fetch the current time + from the X server and use it to activate the frame. + + - The symbol `raise-and-focus', which means to raise the window and + focus it manually. */); + Vx_allow_focus_stealing = Qnewer_time; } From 69abb438b80c18c6409c7423b1fddec8d3da4165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 20 Oct 2022 12:45:31 +0100 Subject: [PATCH 763/771] * lisp/info-look.el (mapc): Add Eglot manual's index. --- lisp/info-look.el | 1 + 1 file changed, 1 insertion(+) diff --git a/lisp/info-look.el b/lisp/info-look.el index ce0a08dcbe6..2eec6f49f5c 100644 --- a/lisp/info-look.el +++ b/lisp/info-look.el @@ -1051,6 +1051,7 @@ Return nil if there is nothing appropriate in the buffer near point." ("eieio" "Function Index") ("gnutls" "(emacs-gnutls)Variable Index" "(emacs-gnutls)Function Index") ("mm" "(emacs-mime)Index") + ("eglot" "Index") ("epa" "Variable Index" "Function Index") ("ert" "Index") ("eshell" "Function and Variable Index") From 785197a07634050b8cb79f1b0c93a16712336529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 20 Oct 2022 12:59:22 +0100 Subject: [PATCH 764/771] Minor fixes to doc/misc/eglot.texi * doc/misc/eglot.texi (eglot-workspace-configuration): Correct markup of eglot-{} (Quick Start): Fix section cross reference. --- doc/misc/eglot.texi | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index c1de1e818dc..6a4127bed7c 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -122,7 +122,7 @@ you want to use Eglot, you just need to make sure those servers are installed on your system. Alternatively, install one or more servers of your choice and add them to the value of @code{eglot-server-programs}, as described in @ref{Setting Up LSP -Server}. +Servers}. @item Turn on Eglot for your project. @@ -969,7 +969,7 @@ object. @var{plist} may be arbitrarity complex, generally containing other keywork-value property sublists corresponding to JSON subobjects. The JSON values @code{true}, @code{false}, @code{null} and @code{@{@}} are represented by the Lisp values @code{t}, @code{:json-false}, -@code{nil}, and @{eglot-@{@}}, respectively. +@code{nil}, and @code{eglot-@{@}}, respectively. @findex eglot-show-workspace-configuration When experimenting with workspace settings, you can use the command @@ -1126,3 +1126,4 @@ that used Eglot to communicate with the language server. @printindex cp @bye + From 8b3a7003274de7b184b71c4552e6c4518948bcfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Thu, 20 Oct 2022 13:49:49 +0100 Subject: [PATCH 765/771] ; fix warning about order of defvaralias/defconst * lisp/progmodes/eglot.el (eglot-{}): Declare alias before thing being aliased. --- lisp/progmodes/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 901bf30d4bd..0a7cb2a9aac 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -406,8 +406,8 @@ This can be useful when using docker to run a language server.") `((1 . eglot-diagnostic-tag-unnecessary-face) (2 . eglot-diagnostic-tag-deprecated-face))) -(defconst eglot--{} (make-hash-table :size 1) "The empty JSON object.") (defvaralias 'eglot-{} 'eglot--{}) +(defconst eglot--{} (make-hash-table :size 1) "The empty JSON object.") (defun eglot--executable-find (command &optional remote) "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26." From a549316c7dce18a47ef88d35aca7d867468432a1 Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Thu, 20 Oct 2022 17:05:01 +0300 Subject: [PATCH 766/771] ; * doc/misc/eglot.texi: Undo some recent "fixes" to the Eglot manual. --- doc/misc/eglot.texi | 57 ++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 6a4127bed7c..25c04940c96 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -122,7 +122,7 @@ you want to use Eglot, you just need to make sure those servers are installed on your system. Alternatively, install one or more servers of your choice and add them to the value of @code{eglot-server-programs}, as described in @ref{Setting Up LSP -Servers}. +Server}. @item Turn on Eglot for your project. @@ -185,10 +185,8 @@ further discussed in this manual; refer to the documentation of the particular server(s) you want to install. To use a language server, Eglot must know how to start it and which -programming languages each server supports. Eglot comes with a fairly -complete set of associations of major-modes to popular language -servers predefined. This information is provided by the -@code{eglot-server-programs} variable. +programming languages each server supports. This information is +provided by the variable @code{eglot-server-programs}. @defvar eglot-server-programs This variable associates major modes with names and command-line @@ -203,13 +201,13 @@ The value of the variable is an alist, whose elements are of the form The @var{major-mode} of the alist elements can be either a symbol of an Emacs major mode or a list of the form @w{@code{(@var{mode} :language-id @var{id})}}, with @var{mode} being a major-mode symbol -and @var{id} a string that identifies the language to the server. The -latter form should be used if Eglot cannot by itself convert the -major-mode to the language identifier string required by the server. -In addition, @var{major-mode} can be a list of several major modes -specified in one of the above forms -- this means a running instance -of the associated server is responsible for files of multiple major -modes or languages in the project. +and @var{id} a string that identifies the language to the server (if +Eglot cannot by itself convert the major-mode to the language +identifier string required by the server). In addition, +@var{major-mode} can be a list of several major modes specified in one +of the above forms -- this means a running instance of the associated +server is responsible for files of multiple major modes or languages +in the project. The @var{server} part of the alist elements can be one of the following: @@ -251,11 +249,13 @@ arguments. @end defvar -If you need to add server associations to the default list, use -@code{add-to-list}. For example, if there is a hypothetical language -server program @command{fools} for the language @code{Foo} which is -supported by an Emacs major-mode @code{foo-mode}, you can add it to -the alist like this: +Eglot comes with a fairly complete set of associations of major-modes +to popular language servers predefined. If you need to add server +associations to the default list, use @code{add-to-list}. For +example, if there is a hypothetical language server program +@command{fools} for the language @code{Foo} which is supported by an +Emacs major-mode @code{foo-mode}, you can add it to the alist like +this: @lisp (add-to-list 'eglot-server-programs @@ -372,7 +372,8 @@ commands and variables. Once Eglot is enabled in a buffer, it uses LSP and the language-server capabilities to activate, enable, and enhance modern IDE features in Emacs. The features themselves are usually provided via other Emacs -packages. These are the main features that Eglot enables and provides: +packages. Here's the list of the main features that Eglot enables and +provides: @itemize @bullet @item @@ -395,7 +396,7 @@ emacs, GNU Emacs Manual}). Eglot provides a backend for the Xref capabilities which uses the language-server understanding of the program source. In particular, it eliminates the need to generate tags tables (@pxref{Tags tables,,, emacs, GNU Emacs Manual}) for -languages that are only supported by the @code{etags} backend. +languages which are only supported by the @code{etags} backend. @item Buffer navigation by name of function, class, method, etc., via Imenu @@ -507,7 +508,7 @@ directory. @item A VC project: source files in a directory hierarchy under some VCS, -e.g.@: a Git repository (@pxref{Version Control,,, emacs, GNU Emacs +e.g.@: a VCS repository (@pxref{Version Control,,, emacs, GNU Emacs Manual}). @item @@ -620,9 +621,8 @@ will be added to those managed by an existing server session. The command attempts to figure out the buffer's major mode and the suitable language server; in case it fails, it might prompt for the major mode to use and for the server program to start. If invoked -with a prefix argument @kbd{C-u}, it always prompts for the server -program, and if invoked with @kbd{C-u C-u}, also prompt for the major -mode. +with @kbd{C-u}, it always prompts for the server program, and if +invoked with @kbd{C-u C-u}, it also prompts for the major mode. If the language server is successfully started and contacted, this command arranges for any other buffers belonging to the same project @@ -637,13 +637,13 @@ Emacs features will be configured to use Eglot, use the @code{eglot-stay-out-of} option (@pxref{Customizing Eglot}). @item M-x eglot-reconnect -Shuts down an the current connection to the language server and -immediately restarts it using the same options used originally. This -can sometimes be useful to unclog a partially malfunctioning server -connection. +This command shuts down the current connection to the language +server and immediately restarts it using the same options used +originally. This can sometimes be useful to unclog a partially +malfunctioning server connection. @item M-x eglot-shutdown -Shuts down a language server. This commands prompts for a language +This command shuts down a language server. It prompts for a language server to shut down (unless there's only one server session, and it manages the current buffer). Then the command shuts down the server and stops managing the buffers the server was used for. Emacs @@ -1126,4 +1126,3 @@ that used Eglot to communicate with the language server. @printindex cp @bye - From c44ea4548da12ad5d43cacbc1f26831bb8c1f7fe Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Thu, 20 Oct 2022 17:08:15 +0300 Subject: [PATCH 767/771] ; * doc/misc/eglot.texi: Fix a typo. --- doc/misc/eglot.texi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 25c04940c96..e5c38a6e3d6 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -122,7 +122,7 @@ you want to use Eglot, you just need to make sure those servers are installed on your system. Alternatively, install one or more servers of your choice and add them to the value of @code{eglot-server-programs}, as described in @ref{Setting Up LSP -Server}. +Servers}. @item Turn on Eglot for your project. From 5c99112e8940d8d4ffef393a3fd05d553b43861b Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Thu, 20 Oct 2022 19:02:14 +0300 Subject: [PATCH 768/771] ; Minor copyedits to eglot.texi. --- doc/misc/eglot.texi | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index e5c38a6e3d6..033464f9909 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -62,14 +62,20 @@ modify this GNU manual.'' @cindex language server protocol Eglot is the Emacs client for the @dfn{Language Server Protocol} (@acronym{LSP}). The name ``Eglot'' is an acronym that stands for -``@emph{E}macs Poly@emph{glot}''.@footnote{ +@ifhtml +``@emph{E}macs Poly@emph{glot}''. +@end ifhtml +@ifnothtml +``Emacs polyGLOT''. +@end ifnothtml +@footnote{ A @dfn{polyglot} is a person who is able to use several languages. } Eglot provides infrastructure and a set of commands for enriching the source code editing capabilities of Emacs via LSP. LSP is a standardized communications protocol between source code editors (such -as Emacs) and language servers, programs external to Emacs for -analyzing source code on behalf of Emacs. The protocol allows Emacs +as Emacs) and language servers---programs external to Emacs which +analyze the source code on behalf of Emacs. The protocol allows Emacs to receive various source code services from the server, such as description and location of functions calls, types of variables, class definitions, syntactic errors, etc. This way, Emacs doesn't need to @@ -417,9 +423,10 @@ activated automatically as you type. @item If a completion package such as @code{company-mode}, a popular -third-party completion package, is installed, Eglot enhances it by -providing completion candidates based on the language-server analysis -of the source code. (@code{company-mode} can be installed from GNU ELPA.) +third-party completion package (or any other completion package), is +installed, Eglot enhances it by providing completion candidates based +on the language-server analysis of the source code. +(@code{company-mode} can be installed from GNU ELPA.) @item If @code{yasnippet}, a popular third-party package for automatic @@ -432,8 +439,9 @@ completion package to instantiate these snippets using If the popular third-party package @code{markdown-mode} is installed, and the server provides at-point documentation formatted as Markdown in addition to plain text, Eglot arranges for the ElDoc package to -enrich this text with e.g. fontification before displaying it to the -user. +enrich this text with fontifications and other nice formatting before +displaying it to the user. This makes the documentation shown by +ElDoc look nicer on display. @item In addition to enabling and enhancing other features and packages, @@ -777,7 +785,9 @@ unexpectedly. The default value 3 means to attempt reconnection only if the previous successful connection lasted for more than that number of seconds; a different positive value changes the minimal length of the connection to trigger reconnection. A value of @code{t} means -always reconnect automatically, and @code{nil} means never reconnect. +always reconnect automatically, and @code{nil} means never reconnect +(in which case you will need to reconnect manually using @kbd{M-x +eglot}). @item eglot-connect-timeout This specifies the number of seconds before connection attempt to a @@ -798,8 +808,7 @@ all during the waiting period. This determines the size of the Eglot events buffer. @xref{Eglot Commands, eglot-events-buffer}, for how to display that buffer. If the value is changed, for it to take effect the connection should be -restarted using @kbd{eglot-shutdown} followed by -@kbd{eglot-reconnect}. +restarted using @kbd{M-x eglot-reconnect}. @c FIXME: Shouldn't the defcustom do this by itself using the :set @c attribute? @xref{Troubleshooting Eglot}, for when this could be useful. @@ -854,6 +863,7 @@ connections, are documented in @ref{Customizing Eglot}. @chapter Customizing Eglot @cindex customizing Eglot +Eglot itself has a relatively small number of customization options. A large part of customizing Eglot to your needs and preferences should actually be done via options of the Emacs packages and features which Eglot supports and enhances (@pxref{Eglot Features}). For example: From 3bab83289415f5169e312064c8fc46a0674b8d9e Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Thu, 20 Oct 2022 20:17:45 +0300 Subject: [PATCH 769/771] Mention Eglot in the Emacs user manual * doc/emacs/maintaining.texi (Xref): * doc/emacs/programs.texi (Symbol Completion, Imenu): Mention Eglot. --- doc/emacs/maintaining.texi | 7 +++++++ doc/emacs/programs.texi | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/doc/emacs/maintaining.texi b/doc/emacs/maintaining.texi index ad4a3ea3506..94171b3a089 100644 --- a/doc/emacs/maintaining.texi +++ b/doc/emacs/maintaining.texi @@ -2094,6 +2094,13 @@ definitions of symbols. (One disadvantage of this kind of backend is that it only knows about subunits that were loaded into the interpreter.) +@item +If Eglot is activated for the current buffer's project +(@pxref{Projects}) and the current buffer's major mode, Eglot consults +an external language server program and provides the data supplied by +the server regarding the definitions of the identifiers in the +project. @xref{Eglot Features,,, eglot, Eglot: The Emacs LSP Client}. + @item An external program can extract references by scanning the relevant files, and build a database of these references. A backend can then diff --git a/doc/emacs/programs.texi b/doc/emacs/programs.texi index 818deb39415..b5e577d96a4 100644 --- a/doc/emacs/programs.texi +++ b/doc/emacs/programs.texi @@ -287,6 +287,13 @@ they occur in the buffer; if you want alphabetic sorting, use the symbol @code{imenu--sort-by-name} as the value. You can also define your own comparison function by writing Lisp code. + If Eglot is activated for the current buffer's project +(@pxref{Projects}) and the current buffer's major mode, Eglot provides +its own facility for producing the buffer's index based on the +analysis of the program source by the language-server which manages +the current buffer. @xref{Eglot Features,,, eglot, Eglot: The Emacs +LSP Client}. + Imenu provides the information to guide Which Function mode @ifnottex (@pxref{Which Function}). @@ -1438,6 +1445,13 @@ uses the available support facilities to come up with the completion candidates: @itemize @bullet +@item +If Eglot is activated for the current buffer's project +(@pxref{Projects}) and the current buffer's major mode, the command +tries to use the corresponding language server for producing the list +of completion candidates. @xref{Eglot Features,,, eglot, Eglot: The +Emacs LSP Client}. + @item If Semantic mode is enabled (@pxref{Semantic}), the command tries to use the Semantic parser data for completion. From 1324baea728a11bf650303698881c682105155da Mon Sep 17 00:00:00 2001 From: Eli Zaretskii Date: Thu, 20 Oct 2022 20:50:34 +0300 Subject: [PATCH 770/771] Add Eglot to the menu bar * lisp/progmodes/eglot.el (eglot): Improve the doc string. * lisp/menu-bar.el (menu-bar-tools-menu): Add Eglot to the menu. --- lisp/menu-bar.el | 4 ++++ lisp/progmodes/eglot.el | 36 ++++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/lisp/menu-bar.el b/lisp/menu-bar.el index 526bccbbac9..849e0f77236 100644 --- a/lisp/menu-bar.el +++ b/lisp/menu-bar.el @@ -1847,6 +1847,10 @@ mail status in mode line")) :help "Toggle automatic parsing in source code buffers (Semantic mode)" :button (:toggle . (bound-and-true-p semantic-mode)))) + (bindings--define-key menu [eglot] + '(menu-item "Language Server Support (Eglot)" eglot + :help "Start language server suitable for this buffer's major-mode")) + (bindings--define-key menu [ede] '(menu-item "Project Support (EDE)" global-ede-mode diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 0a7cb2a9aac..ada8b01fec2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -1039,26 +1039,30 @@ suitable root directory for a given LSP server's purposes." ;;;###autoload (defun eglot (managed-major-mode project class contact language-id &optional interactive) - "Manage a project with a Language Server Protocol (LSP) server. + "Start LSP server in support of PROJECT's buffers under MANAGED-MAJOR-MODE. + +This starts a Language Server Protocol (LSP) server suitable for the +buffers of PROJECT whose `major-mode' is MANAGED-MAJOR-MODE. +CLASS is the class of the LSP server to start and CONTACT specifies +how to connect to the server. + +Interactively, the command attempts to guess MANAGED-MAJOR-MODE +from the current buffer's `major-mode', CLASS and CONTACT from +`eglot-server-programs' looked up by the major mode, and PROJECT from +`project-find-functions'. The search for active projects in this +context binds `eglot-lsp-context' (which see). + +If it can't guess, it prompts the user for the mode and the server. +With a single \\[universal-argument] prefix arg, it always prompts for COMMAND. +With two \\[universal-argument], it also always prompts for MANAGED-MAJOR-MODE. The LSP server of CLASS is started (or contacted) via CONTACT. If this operation is successful, current *and future* file buffers of MANAGED-MAJOR-MODE inside PROJECT become \"managed\" -by the LSP server, meaning information about their contents is -exchanged periodically to provide enhanced code-analysis via -`xref-find-definitions', `flymake-mode', `eldoc-mode', -`completion-at-point', among others. - -Interactively, the command attempts to guess MANAGED-MAJOR-MODE -from current buffer, CLASS and CONTACT from -`eglot-server-programs' and PROJECT from -`project-find-functions'. The search for active projects in this -context binds `eglot-lsp-context' (which see). - -If it can't guess, the user is prompted. With a single -\\[universal-argument] prefix arg, it always prompt for COMMAND. -With two \\[universal-argument] prefix args, also prompts for -MANAGED-MAJOR-MODE. +by the LSP server, meaning the information about their contents is +exchanged periodically with the server to provide enhanced +code-analysis via `xref-find-definitions', `flymake-mode', +`eldoc-mode', and `completion-at-point', among others. PROJECT is a project object as returned by `project-current'. From 937ae0cf55d31c332fba3d96061e2ac3653e5437 Mon Sep 17 00:00:00 2001 From: Filipp Gunbin Date: Thu, 20 Oct 2022 20:41:00 +0300 Subject: [PATCH 771/771] Fix ldapsearch output parsing in ldap-search-internal * lisp/net/ldap.el (ldap-search-internal): When parsing output, make sure that file:// matched before opening the file. (bug#58605) --- lisp/net/ldap.el | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lisp/net/ldap.el b/lisp/net/ldap.el index 062ff05d69c..ccad8c4edb1 100644 --- a/lisp/net/ldap.el +++ b/lisp/net/ldap.el @@ -715,14 +715,14 @@ an alist of attribute/value pairs." (eq (string-match "/\\(.:.*\\)$" value) 0)) (setq value (match-string 1 value))) ;; Do not try to open non-existent files - (if (equal value "") - (setq value " ") - (with-current-buffer bufval + (if (match-string 3) + (with-current-buffer bufval (erase-buffer) (set-buffer-multibyte nil) (insert-file-contents-literally value) (delete-file value) - (setq value (buffer-string)))) + (setq value (buffer-string))) + (setq value " ")) (setq record (cons (list name value) record)) (forward-line 1))