[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/eglot 6f1ecc6 28/69: Merge branch use-eieio-server-defc
From: |
João Távora |
Subject: |
[elpa] externals/eglot 6f1ecc6 28/69: Merge branch use-eieio-server-defclass into jsonrpc-refactor |
Date: |
Fri, 22 Jun 2018 11:54:58 -0400 (EDT) |
branch: externals/eglot
commit 6f1ecc6521cc70015252749228e81d58163ed5f9
Merge: 0e44b27 89baadf
Author: João Távora <address@hidden>
Commit: João Távora <address@hidden>
Merge branch use-eieio-server-defclass into jsonrpc-refactor
---
eglot-tests.el | 55 +++--
eglot.el | 600 ++++++++++++++++++++++++++++++-------------------------
jsonrpc-tests.el | 6 +-
jsonrpc.el | 257 +++++++++++++-----------
4 files changed, 502 insertions(+), 416 deletions(-)
diff --git a/eglot-tests.el b/eglot-tests.el
index a19f121..8afbfa5 100644
--- a/eglot-tests.el
+++ b/eglot-tests.el
@@ -51,22 +51,24 @@
(defun eglot--call-with-dirs-and-files (dirs fn)
(let* ((default-directory (make-temp-file "eglot--fixture" t))
- new-buffers new-processes)
+ new-buffers new-servers)
(unwind-protect
(let ((find-file-hook
(cons (lambda () (push (current-buffer) new-buffers))
find-file-hook))
(eglot-connect-hook
- (lambda (proc) (push proc new-processes))))
+ (lambda (server) (push server new-servers))))
(mapc #'eglot--make-file-or-dirs dirs)
(funcall fn))
(eglot--message "Killing buffers %s, deleting %s, killing %s"
(mapconcat #'buffer-name new-buffers ", ")
default-directory
- new-processes)
+ (mapcar #'jsonrpc-name new-servers))
(let ((eglot-autoreconnect nil))
(mapc #'eglot-shutdown
- (cl-remove-if-not #'process-live-p new-processes)))
+ (cl-remove-if-not
+ (lambda (server) (process-live-p (jsonrpc--process server)))
+ new-servers)))
(dolist (buf new-buffers) ;; have to save otherwise will get prompted
(with-current-buffer buf (save-buffer) (kill-buffer)))
(delete-directory default-directory 'recursive))))
@@ -78,8 +80,7 @@
(defun eglot--call-with-test-timeout (timeout fn)
(let* ((tag (make-symbol "tag"))
(timed-out (make-symbol "timeout"))
- (timer )
- (jsonrpc-request-timeout 1)
+ (timer)
(retval))
(unwind-protect
(setq retval
@@ -112,7 +113,7 @@
(ert-deftest auto-detect-running-server ()
"Visit a file and M-x eglot, then visit a neighbour. "
(skip-unless (executable-find "rls"))
- (let (proc)
+ (let (server)
(eglot--with-dirs-and-files
'(("project" . (("coiso.rs" . "bla")
("merdix.rs" . "bla")))
@@ -120,42 +121,40 @@
(eglot--with-test-timeout 2
(with-current-buffer
(eglot--find-file-noselect "project/coiso.rs")
- (setq proc
- (eglot 'rust-mode `(transient . ,default-directory)
- '("rls")))
- (should (jsonrpc-current-process)))
+ (should (setq server (apply #'eglot (eglot--interactive))))
+ (should (jsonrpc-current-connection)))
(with-current-buffer
(eglot--find-file-noselect "project/merdix.rs")
- (should (jsonrpc-current-process))
- (should (eq (jsonrpc-current-process) proc)))
+ (should (jsonrpc-current-connection))
+ (should (eq (jsonrpc-current-connection) server)))
(with-current-buffer
(eglot--find-file-noselect "anotherproject/cena.rs")
- (should-error (jsonrpc-current-process-or-lose)))))))
+ (should-error (jsonrpc-current-connection-or-lose)))))))
(ert-deftest auto-reconnect ()
"Start a server. Kill it. Watch it reconnect."
(skip-unless (executable-find "rls"))
- (let (proc
- (eglot-autoreconnect 1))
+ (let (server (eglot-autoreconnect 1))
(eglot--with-dirs-and-files
'(("project" . (("coiso.rs" . "bla")
("merdix.rs" . "bla"))))
(eglot--with-test-timeout 3
(with-current-buffer
(eglot--find-file-noselect "project/coiso.rs")
- (setq proc
- (eglot 'rust-mode `(transient . ,default-directory)
- '("rls")))
+ (should (setq server (apply #'eglot (eglot--interactive))))
;; In 1.2 seconds > `eglot-autoreconnect' kill servers. We
;; should have a automatic reconnection.
- (run-with-timer 1.2 nil (lambda () (delete-process proc)))
- (while (process-live-p proc) (accept-process-output nil 0.5))
- (should (jsonrpc-current-process))
+ (run-with-timer 1.2 nil (lambda () (delete-process
+ (jsonrpc--process server))))
+ (while (process-live-p (jsonrpc--process server))
+ (accept-process-output nil 0.5))
+ (should (jsonrpc-current-connection))
;; Now try again too quickly
- (setq proc (jsonrpc-current-process))
- (run-with-timer 0.5 nil (lambda () (delete-process proc)))
- (while (process-live-p proc) (accept-process-output nil 0.5))
- (should (not (jsonrpc-current-process))))))))
+ (setq server (jsonrpc-current-connection))
+ (let ((proc (jsonrpc--process server)))
+ (run-with-timer 0.5 nil (lambda () (delete-process proc)))
+ (while (process-live-p proc) (accept-process-output nil 0.5)))
+ (should (not (jsonrpc-current-connection))))))))
(ert-deftest basic-completions ()
"Test basic autocompletion in a python LSP"
@@ -165,7 +164,7 @@
(eglot--with-test-timeout 4
(with-current-buffer
(eglot--find-file-noselect "project/something.py")
- (eglot 'python-mode `(transient . ,default-directory) '("pyls"))
+ (should (apply #'eglot (eglot--interactive)))
(goto-char (point-max))
(completion-at-point)
(should (looking-back "sys.exit"))))))
@@ -178,7 +177,7 @@
(eglot--with-test-timeout 4
(with-current-buffer
(eglot--find-file-noselect "project/something.py")
- (eglot 'python-mode `(transient . ,default-directory) '("pyls"))
+ (should (apply #'eglot (eglot--interactive)))
(goto-char (point-max))
(setq eldoc-last-message nil)
(completion-at-point)
diff --git a/eglot.el b/eglot.el
index 020e352..1328331 100644
--- a/eglot.el
+++ b/eglot.el
@@ -69,15 +69,31 @@
: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"))
(php-mode . ("php" "vendor/felixfbecker/\
language-server/bin/php-language-server.php")))
- "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.")
+ "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)))
@@ -94,85 +110,126 @@ lasted more than that many seconds."
(integer :tag "Number of 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-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.")
+
+(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))))
+
+(defclass eglot-lsp-server (jsonrpc-process-connection)
+ ((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)
+ (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)
+ (inhibit-autoreconnect
+ :documentation "Generalized boolean inhibiting auto-reconnection if true."
+ :initarg :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))
+ :documentation
+ "Represents a server. Wraps a process for LSP communication.")
+
+
;;; 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.")
-(jsonrpc-define-process-var eglot--major-mode nil
- "The major-mode this server is managing.")
-
-(jsonrpc-define-process-var eglot--capabilities :unreported
- "Holds list of capabilities that server reported")
-
-(jsonrpc-define-process-var eglot--project nil
- "The project the server belongs to.")
-
-(jsonrpc-define-process-var eglot--spinner `(nil nil t)
- "\"Spinner\" used by some servers.
-A list (ID WHAT DONE-P).")
-
-(jsonrpc-define-process-var eglot--moribund nil
- "Non-nil if server is about to exit")
-
-(jsonrpc-define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect
- "If non-nil, don't autoreconnect on unexpected quit.")
-
-(jsonrpc-define-process-var eglot--file-watches (make-hash-table :test #'equal)
- "File system watches for the didChangeWatchedfiles thingy.")
-
-(defun eglot--on-shutdown (proc)
- "Called by jsonrpc.el when PROC is already dead."
+(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 (jsonrpc-current-connection-or-lose) t))
+ (eglot--message "Asking %s politely to terminate" (jsonrpc-name server))
+ (unwind-protect
+ (progn
+ (setf (eglot--moribund server) t)
+ (jsonrpc-request server :shutdown nil :timeout 3)
+ ;; this one is supposed to always fail, 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)))
+ (when (process-live-p (jsonrpc--process server))
+ (eglot--warn "Brutally deleting non-compliant server %s" (jsonrpc-name
server))
+ (delete-process (jsonrpc--process server)))))
+
+(defun eglot--on-shutdown (server)
+ "Called by jsonrpc.el when SERVER is already dead."
;; Turn off `eglot--managed-mode' where appropriate.
- (dolist (buffer (eglot--managed-buffers proc))
- (with-current-buffer buffer (eglot--managed-mode-onoff proc -1)))
+ (dolist (buffer (eglot--managed-buffers server))
+ (with-current-buffer buffer (eglot--managed-mode-onoff server -1)))
;; Kill any expensive watches
(maphash (lambda (_id watches)
(mapcar #'file-notify-rm-watch watches))
- (eglot--file-watches proc))
+ (eglot--file-watches server))
;; 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))
+ (setf (gethash (eglot--project server) eglot--servers-by-project)
+ (delq server
+ (gethash (eglot--project server) eglot--servers-by-project)))
+ (cond ((eglot--moribund server))
+ ((not (eglot--inhibit-autoreconnect server))
(eglot--warn "Reconnecting after unexpected server exit.")
- (eglot-reconnect proc))
- ((timerp (eglot--inhibit-autoreconnect proc))
+ (eglot-reconnect server))
+ ((timerp (eglot--inhibit-autoreconnect server))
(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 (jsonrpc-current-process-or-lose) t))
- (eglot--message "Asking %s politely to terminate" proc)
- (unwind-protect
- (progn
- (setf (eglot--moribund proc) t)
- (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.
- (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 %s" proc)
- (delete-process proc))))
-
-(defun eglot--find-current-process ()
- "The current logical EGLOT process."
- (let* ((probe (or (project-current) `(transient . ,default-directory))))
- (cl-find major-mode (gethash probe eglot--processes-by-project)
- :key #'eglot--major-mode)))
-
-(jsonrpc-define-process-var eglot--managed-buffers nil
- "Buffers managed by the server.")
-
-(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))
@@ -181,29 +238,6 @@ called interactively."
(push sym retval))))
retval))
-(defun eglot--client-capabilities ()
- "What the EGLOT LSP client supports."
- (jsonrpc-obj
- :workspace (jsonrpc-obj
- :applyEdit t
- :workspaceEdit `(:documentChanges :json-false)
- :didChangeWatchesFiles `(:dynamicRegistration t)
- :symbol `(:dynamicRegistration :json-false))
- :textDocument (jsonrpc-obj
- :synchronization (jsonrpc-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 (jsonrpc-obj)))
-
(defvar eglot--command-history nil
"History of CONTACT arguments to `eglot'.")
@@ -221,37 +255,40 @@ called interactively."
(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 (if (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 "[eglot] Enter program to execute (or <host>:<port>): ")
(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 (cons class contact) t)))
;;;###autoload
(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
+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
\"managed\" by the LSP server, meaning information about their
@@ -268,106 +305,119 @@ prefix args, also prompts for MANAGED-MAJOR-MODE.
PROJECT is a project instance as returned by `project-current'.
-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.
+CONTACT specifies how to contact the server. It can be:
+
+* 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.
-MANAGED-MAJOR-MODE is an Emacs major mode.
+* 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.
INTERACTIVE is t if called interactively."
(interactive (eglot--interactive))
- (let* ((short-name (eglot--project-short-name project)))
- (let ((current-process (jsonrpc-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* ((nickname (file-name-base (directory-file-name
+ (car (project-roots project)))))
+ (current-server (jsonrpc-current-connection))
+ (live-p (and current-server
+ (process-live-p (jsonrpc--process current-server)))))
+ (if (and live-p
+ interactive
+ (y-or-n-p "[eglot] Live process found, reconnect instead? "))
+ (eglot-reconnect current-server interactive)
+ (when live-p (eglot-shutdown current-server))
+ (let ((server (eglot--connect project
managed-major-mode
- (format "%s/%s" short-name
managed-major-mode)
+ (format "%s/%s" nickname
managed-major-mode)
+ nickname
contact)))
- (eglot--message "Connected! Process `%s' now \
+ (eglot--message "Connected! Process `%s' now \
managing `%s' buffers in project `%s'."
- proc managed-major-mode short-name)
- proc)))))
+ (jsonrpc-name server) managed-major-mode
+ nickname)
+ 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 (jsonrpc-current-process-or-lose) t))
- (when (process-live-p process)
- (eglot-shutdown process interactive))
- (eglot--connect (eglot--project process)
- (eglot--major-mode process)
- (jsonrpc-name process)
- (jsonrpc-contact process))
+ (interactive (list (jsonrpc-current-connection-or-lose) t))
+ (when (process-live-p (jsonrpc--process server))
+ (eglot-shutdown server interactive))
+ (eglot--connect (eglot--project server)
+ (eglot--major-mode server)
+ (jsonrpc-name server)
+ (eglot--project-nickname server)
+ (jsonrpc-contact 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 (proc method id params)
+(defun eglot--dispatch (server method id params)
"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))))
- (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)
+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)
+ "Connect to PROJECT, MANAGED-MAJOR-MODE, NAME.
+And NICKNAME and CONTACT."
(let* ((contact (if (functionp contact) (funcall contact) contact))
- (proc
+ (server
(jsonrpc-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))
- (run-hook-with-args 'eglot-connect-hook proc)
+ (setf (eglot--project server) project)
+ (setf (eglot--project-nickname server) nickname)
+ (setf (eglot--major-mode server) managed-major-mode)
+ (push server (gethash project eglot--servers-by-project))
+ (run-hook-with-args 'eglot-connect-hook server)
(unwind-protect
(cl-destructuring-bind (&key capabilities)
(jsonrpc-request
- proc
+ server
:initialize
- (jsonrpc-obj :processId (unless (eq (process-type proc)
+ (jsonrpc-obj :processId (unless (eq (process-type
+ (jsonrpc--process server))
'network)
(emacs-pid))
:rootPath (expand-file-name
(car (project-roots project)))
:rootUri (eglot--path-to-uri
(car (project-roots project)))
- :initializationOptions []
- :capabilities (eglot--client-capabilities)))
- (setf (eglot--capabilities proc) capabilities)
- (setf (jsonrpc-status proc) nil)
+ :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 proc)))
- (jsonrpc-notify proc :initialized (jsonrpc-obj :__dummy__ t))
- (setf (eglot--inhibit-autoreconnect proc)
+ (eglot--maybe-activate-editing-mode server)))
+ (jsonrpc-notify server :initialized (jsonrpc-obj :__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 proc)
+ (setf (eglot--inhibit-autoreconnect server)
(null eglot-autoreconnect)))))))
- (setq success proc))
- (unless (or success (not (process-live-p proc)) (eglot--moribund proc))
- (eglot-shutdown proc)))))
-
-(defun eglot--server-ready-p (_what _proc)
- "Tell if server of PROC ready for processing deferred WHAT."
- (not (eglot--outstanding-edits-p)))
+ (setq success server))
+ (unless (or success (not (process-live-p (jsonrpc--process server)))
+ (eglot--moribund server))
+ (eglot-shutdown server)))))
;;; Helpers
@@ -435,7 +485,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 (jsonrpc-current-process-or-lose))
+ (cl-loop for caps = (eglot--capabilities
(jsonrpc-current-connection-or-lose))
then (cadr probe)
for feat in feats
for probe = (plist-member caps feat)
@@ -460,7 +510,7 @@ If optional MARKERS, make markers."
nil nil eglot-mode-map
(cond
(eglot--managed-mode
- (add-hook 'jsonrpc-find-process-functions 'eglot--find-current-process nil
t)
+ (add-hook 'jsonrpc-find-connection-functions 'eglot--find-current-server
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)
@@ -475,7 +525,7 @@ If optional MARKERS, make markers."
#'eglot-eldoc-function)
(add-function :around (local imenu-create-index-function) #'eglot-imenu))
(t
- (remove-hook 'jsonrpc-find-process-functions 'eglot--find-current-process
t)
+ (remove-hook 'jsonrpc-find-connection-functions
'eglot--find-current-server 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)
@@ -490,14 +540,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)
@@ -505,15 +555,21 @@ 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--find-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--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--find-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--find-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))))
@@ -551,16 +607,17 @@ Uses THING, FACE, DEFS and PREPEND."
(defun eglot--mode-line-format ()
"Compose the EGLOT's mode-line."
- (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 (jsonrpc-status proc))))
+ (pcase-let* ((server (jsonrpc-current-connection))
+ (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)))
+ (`(,status ,serious-p) (and server (jsonrpc-status server))))
(append
`(,(eglot--mode-line-props "eglot" 'eglot-mode-line nil))
- (when name
+ (when nick
`(":" ,(eglot--mode-line-props
- name 'eglot-mode-line
+ nick'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")))
@@ -568,7 +625,7 @@ Uses THING, FACE, DEFS and PREPEND."
`("/" ,(eglot--mode-line-props
"error" 'compilation-mode-line-fail
'((mouse-1 eglot-events-buffer "go to events buffer")
- (mouse-3 jrpc-clear-status "clear this status"))
+ (mouse-3 jsonrpc-clear-status "clear this status"))
(format "An error occured: %s\n" status))))
,@(when (and doing (not done-p))
`("/" ,(eglot--mode-line-props
@@ -580,9 +637,8 @@ Uses THING, FACE, DEFS and PREPEND."
`("/" ,(eglot--mode-line-props
(format "%d oustanding requests" pending) 'warning
'((mouse-1 eglot-events-buffer "go to events buffer")
- (mouse-3 jrpc-forget-pending-continuations
- "fahgettaboudit"))
- (format "%d pending requests\n" pending)))))))))
+ (mouse-3 jsonrpc-forget-pending-continuations
+ "fahgettaboudit"))))))))))
(add-to-list 'mode-line-misc-info
`(eglot--managed-mode (" [" eglot--mode-line-format "] ")))
@@ -590,14 +646,15 @@ Uses THING, FACE, DEFS and PREPEND."
;;; Protocol implementation (Requests, notifications, etc)
;;;
-(cl-defun eglot--server-window/showMessage (_process &key type message)
+(cl-defmethod eglot-handle-notification
+ (_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-defun eglot--server-window/showMessageRequest
- (process &key id type message actions)
+(cl-defmethod eglot-handle-request
+ (server id (_method (eql :window/showMessageRequest)) &key type message
actions)
"Handle server request window/showMessageRequest"
(let (reply)
(unwind-protect
@@ -612,22 +669,24 @@ Uses THING, FACE, DEFS and PREPEND."
'("OK"))
nil t (plist-get (elt actions 0) :title)))
(if reply
- (jsonrpc-reply process id :result (jsonrpc-obj :title reply))
- (jsonrpc-reply process id
+ (jsonrpc-reply server id :result (jsonrpc-obj :title reply))
+ (jsonrpc-reply server id
:error (jsonrpc-obj :code -32800
:message "User cancelled"))))))
-(cl-defun eglot--server-window/logMessage (_proc &key _type _message)
+(cl-defmethod eglot-handle-notification
+ (_server (_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
+ (_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-defun eglot--server-textDocument/publishDiagnostics
- (_proc &key uri diagnostics)
+(cl-defmethod eglot-handle-notification
+ (_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
@@ -651,42 +710,43 @@ Uses THING, FACE, DEFS and PREPEND."
(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'.
+(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))
(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))
+ server :id id registerOptions))
(unless (eq t (car retval))
(cl-return-from eglot--register-unregister
(jsonrpc-reply
- proc jsonrpc-id
+ server jsonrpc-id
:error `(:code -32601 :message ,(or (cadr retval)
"sorry")))))))))
- (jsonrpc-reply proc jsonrpc-id :result (jsonrpc-obj :message "OK")))
+ (jsonrpc-reply server jsonrpc-id :result (jsonrpc-obj :message "OK")))
-(cl-defun eglot--server-client/registerCapability
- (proc &key id registrations)
+(cl-defmethod eglot-handle-request
+ (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-defun eglot--server-client/unregisterCapability
- (proc &key id unregisterations) ;; XXX: Yeah, typo and all.. See spec...
+(cl-defmethod eglot-handle-request
+ (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-defun eglot--server-workspace/applyEdit
- (proc &key id _label edit)
+(cl-defmethod eglot-handle-request
+ (server id (_method (eql :workspace/applyEdit)) &key _label edit)
"Handle server request workspace/applyEdit"
(condition-case err
(progn (eglot--apply-workspace-edit edit 'confirm)
- (jsonrpc-reply proc id :result `(:applied )))
- (error (jsonrpc-reply proc id
+ (jsonrpc-reply server id :result `(:applied )))
+ (error (jsonrpc-reply server id
:result `(:applied :json-false)
- :error (jsonrpc-obj :code -32001
- :message (format "%s" err))))))
+ :error (eglot--obj :code -32001
+ :message (format "%s" err))))))
(defun eglot--TextDocumentIdentifier ()
"Compute TextDocumentIdentifier object for current buffer."
@@ -725,6 +785,13 @@ THINGS are either registrations or unregisterations."
(cl-plusp (+ (length (car eglot--recent-changes))
(length (cdr eglot--recent-changes)))))
+(cl-defmethod jsonrpc-connection-ready-p ((_server eglot-lsp-server) _what)
+ "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."
+ (and (cl-call-next-method)
+ (not (eglot--outstanding-edits-p))))
+
(defun eglot--before-change (start end)
"Hook onto `before-change-functions'.
Records START and END, crucially convert them into
@@ -757,7 +824,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 (jsonrpc-current-process-or-lose))
+ (let* ((server (jsonrpc-current-connection-or-lose))
(sync-kind (eglot--server-capable :textDocumentSync))
(emacs-messup (/= (length (car eglot--recent-changes))
(length (cdr eglot--recent-changes))))
@@ -767,7 +834,7 @@ Records START, END and PRE-CHANGE-LENGTH locally."
(save-restriction
(widen)
(jsonrpc-notify
- proc :textDocument/didChange
+ server :textDocument/didChange
(jsonrpc-obj
:textDocument
(eglot--VersionedTextDocumentIdentifier)
@@ -783,38 +850,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))
+ (setf (eglot--spinner server) (list nil :textDocument/didChange t))
;; HACK!
- (jsonrpc--call-deferred proc))))
+ (jsonrpc--call-deferred server))))
(defun eglot--signal-textDocument/didOpen ()
"Send textDocument/didOpen to server."
(setq eglot--recent-changes (cons [] []))
(jsonrpc-notify
- (jsonrpc-current-process-or-lose)
+ (jsonrpc-current-connection-or-lose)
:textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem))))
(defun eglot--signal-textDocument/didClose ()
"Send textDocument/didClose to server."
(jsonrpc-notify
- (jsonrpc-current-process-or-lose)
+ (jsonrpc-current-connection-or-lose)
:textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier))))
(defun eglot--signal-textDocument/willSave ()
"Send textDocument/willSave to server."
- (let ((proc (jsonrpc-current-process-or-lose))
+ (let ((server (jsonrpc-current-connection-or-lose))
(params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier))))
- (jsonrpc-notify proc :textDocument/willSave params)
+ (jsonrpc-notify server :textDocument/willSave params)
(when (eglot--server-capable :textDocumentSync :willSaveWaitUntil)
(ignore-errors
(eglot--apply-text-edits
- (jsonrpc-request proc :textDocument/willSaveWaituntil params
+ (jsonrpc-request server :textDocument/willSaveWaituntil params
:timeout 0.5))))))
(defun eglot--signal-textDocument/didSave ()
"Send textDocument/didSave to server."
(jsonrpc-notify
- (jsonrpc-current-process-or-lose)
+ (jsonrpc-current-connection-or-lose)
:textDocument/didSave
(jsonrpc-obj
;; TODO: Handle TextDocumentSaveRegistrationOptions to control this.
@@ -856,7 +923,7 @@ DUMMY is ignored"
(cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot)))
(when (eglot--server-capable :documentSymbolProvider)
- (let ((proc (jsonrpc-current-process-or-lose))
+ (let ((server (jsonrpc-current-connection-or-lose))
(text-id (eglot--TextDocumentIdentifier)))
(completion-table-with-cache
(lambda (string)
@@ -873,7 +940,7 @@ DUMMY is ignored"
:locations (list location)
:kind kind
:containerName containerName))
- (jsonrpc-request proc
+ (jsonrpc-request server
:textDocument/documentSymbol
(jsonrpc-obj
:textDocument text-id))))
@@ -891,7 +958,7 @@ DUMMY is ignored"
(location-or-locations
(if rich-identifier
(get-text-property 0 :locations rich-identifier)
- (jsonrpc-request (jsonrpc-current-process-or-lose)
+ (jsonrpc-request (jsonrpc-current-connection-or-lose)
:textDocument/definition
(get-text-property
0 :textDocumentPositionParams identifier)))))
@@ -911,7 +978,7 @@ DUMMY is ignored"
(mapcar
(jsonrpc-lambda (&key uri range)
(eglot--xref-make identifier uri (plist-get range :start)))
- (jsonrpc-request (jsonrpc-current-process-or-lose)
+ (jsonrpc-request (jsonrpc-current-connection-or-lose)
:textDocument/references
(append
params
@@ -924,21 +991,21 @@ 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-process-or-lose)
+ (jsonrpc-request (jsonrpc-current-connection-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 (jsonrpc-current-process-or-lose)))
+ (server (jsonrpc-current-connection-or-lose)))
(when (eglot--server-capable :completionProvider)
(list
(or (car bounds) (point))
(or (cdr bounds) (point))
(completion-table-with-cache
(lambda (_ignored)
- (let* ((resp (jsonrpc-request proc
+ (let* ((resp (jsonrpc-request server
:textDocument/completion
(eglot--TextDocumentPositionParams)
:deferred :textDocument/completion))
@@ -969,7 +1036,7 @@ DUMMY is ignored"
(or (get-text-property 0 :documentation obj)
(and (eglot--server-capable :completionProvider
:resolveProvider)
- (plist-get (jsonrpc-request proc
:completionItem/resolve
+ (plist-get (jsonrpc-request server
:completionItem/resolve
(text-properties-at 0
obj))
:documentation)))))
(when documentation
@@ -1015,7 +1082,7 @@ DUMMY is ignored"
"Request \"hover\" information for the thing at point."
(interactive)
(cl-destructuring-bind (&key contents range)
- (jsonrpc-request (jsonrpc-current-process-or-lose) :textDocument/hover
+ (jsonrpc-request (jsonrpc-current-connection-or-lose) :textDocument/hover
(eglot--TextDocumentPositionParams))
(when (seq-empty-p contents) (eglot--error "No hover info here"))
(with-help-window "*eglot help*"
@@ -1026,7 +1093,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 (jsonrpc-current-process-or-lose))
+ (server (jsonrpc-current-connection-or-lose))
(position-params (eglot--TextDocumentPositionParams))
sig-showing)
(cl-macrolet ((when-buffer-window
@@ -1034,7 +1101,7 @@ If SKIP-SIGNATURE, don't try to send
textDocument/signatureHelp."
(with-current-buffer buffer ,@body))))
(when (eglot--server-capable :signatureHelpProvider)
(jsonrpc-async-request
- proc :textDocument/signatureHelp position-params
+ server :textDocument/signatureHelp position-params
:success-fn
(jsonrpc-lambda (&key signatures activeSignature
activeParameter)
@@ -1047,7 +1114,7 @@ If SKIP-SIGNATURE, don't try to send
textDocument/signatureHelp."
:deferred :textDocument/signatureHelp))
(when (eglot--server-capable :hoverProvider)
(jsonrpc-async-request
- proc :textDocument/hover position-params
+ server :textDocument/hover position-params
:success-fn (jsonrpc-lambda (&key contents range)
(unless sig-showing
;; for eglot-tests.el's sake, set this unconditionally
@@ -1057,7 +1124,7 @@ If SKIP-SIGNATURE, don't try to send
textDocument/signatureHelp."
:deferred :textDocument/hover))
(when (eglot--server-capable :documentHighlightProvider)
(jsonrpc-async-request
- proc :textDocument/documentHighlight position-params
+ server :textDocument/documentHighlight position-params
:success-fn
(lambda (highlights)
(mapc #'delete-overlay eglot--highlights)
@@ -1085,7 +1152,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-process-or-lose)
+ (jsonrpc-request (jsonrpc-current-connection-or-lose)
:textDocument/documentSymbol
(jsonrpc-obj
:textDocument
(eglot--TextDocumentIdentifier))))))
@@ -1153,7 +1220,7 @@ Proceed? "
(unless (eglot--server-capable :renameProvider)
(eglot--error "Server can't rename!"))
(eglot--apply-workspace-edit
- (jsonrpc-request (jsonrpc-current-process-or-lose)
+ (jsonrpc-request (jsonrpc-current-connection-or-lose)
:textDocument/rename
`(,@(eglot--TextDocumentPositionParams)
,@(jsonrpc-obj :newName newname)))
current-prefix-arg))
@@ -1161,9 +1228,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
@@ -1178,7 +1245,7 @@ Proceed? "
(expand-file-name glob))
f))))
(jsonrpc-notify
- proc :workspace/didChangeWatchedFiles
+ server :workspace/didChangeWatchedFiles
`(:changes ,(vector `(:uri ,(eglot--path-to-uri file)
:type ,(cl-case action
(created 1)
@@ -1190,40 +1257,37 @@ 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."
- (or (eq what :textDocument/completion) ; RLS normally ready for this
- ; one, even if building ;
- (pcase-let ((`(,_id ,what ,done ,_detail) (eglot--spinner proc)))
- (and (equal "Indexing" what) done))))
-
-;;;###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 '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)
+(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 process) (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 process))
+ (dolist (buffer (eglot--managed-buffers server))
(with-current-buffer buffer
(funcall (or eglot--current-flymake-report-fn #'ignore)
eglot--unreported-diagnostics)))))
diff --git a/jsonrpc-tests.el b/jsonrpc-tests.el
index a3de884..a112063 100644
--- a/jsonrpc-tests.el
+++ b/jsonrpc-tests.el
@@ -36,7 +36,8 @@
:name "Emacs RPC server" :server t :host "localhost"
:service 44444
:log (lambda (_server client _message)
(jsonrpc-connect
- (process-name client) client
+ (process-name client)
+ (make-instance 'jsonrpc-process-connection
:process client)
(lambda (endpoint method id params)
(unless (memq method '(+ - * / vconcat append
sit-for))
(signal 'jsonrpc-error
`((jsonrpc-error-message
@@ -52,7 +53,8 @@
,@body
(unwind-protect
(delete-process ,server)
- (delete-process (jsonrpc--process ,endpoint-sym)))))))
+ (delete-process
+ (jsonrpc--process ,endpoint-sym)))))))
(ert-deftest returns-3 ()
"returns 3"
diff --git a/jsonrpc.el b/jsonrpc.el
index a380b7a..5a869aa 100644
--- a/jsonrpc.el
+++ b/jsonrpc.el
@@ -45,14 +45,14 @@
;; sockets). This uses some simple HTTP-style envelopping for JSON
;; objects travelling through the wire.
;;
-;; Thus, the main entry point `jsonrpc-connect', returns one of these
-;; objects by default. It is passed a name identifying the connection
-;; and a "contact", which will determine the connection type to make.
-;; This contact can a list of strings (a command and arguments for
-;; creating subprocesses) or a list of the form (HOST PORT-NUMBER
-;; PARAMS...) for connecting via TCP. For the providing the
-;; aforementioned flexibility, it can also be a any object of a
-;; subclass of `jsonrpc-connection'.
+;; The entry point `jsonrpc-connect', returns one of these objects by
+;; default. It is passed a name identifying the connection and a
+;; "contact", which will determine the connection type to make. This
+;; contact can a list of strings (a command and arguments for creating
+;; subprocesses) or a list of the form (HOST PORT-NUMBER PARAMS...)
+;; for connecting via TCP. For the providing the aforementioned
+;; flexibility, it can also be a any object of a subclass of
+;; `jsonrpc-connection'.
;;
;; `jsonrpc-connect' returns a connection upon connection. This value
;; should be saved to be later given to `jsonrpc-notify',
@@ -89,40 +89,41 @@
;; Finally, here's an example Emacs JSONRPC server that offers a (very
;; small) subset of Elisp for remote calling:
;;
-;; (defvar server-server) (defvar server-endpoint)
-;; (defvar server-allowed-functions '(+ - * / vconcat append sit-for))
+;; (defvar server-server) (defvar server-endpoint)
+;; (defvar server-allowed-functions '(+ - * / vconcat append sit-for))
;;
-;; (setq server-server
-;; (make-network-process
-;; :name "Emacs RPC server" :server t :host "localhost" :service 9393
-;; :log (lambda (_server client _message)
-;; (jsonrpc-connect
-;; (process-name client) client
-;; (lambda (endpoint method id params)
-;; (unless (memq method server-allowed-functions)
-;; (signal 'jsonrpc-error `((jsonrpc-error-message
-;; . "Sorry, this isn't
allowed")
-;; (jsonrpc-error-code .
-32601))))
-;; (jsonrpc-reply endpoint id :result
-;; (apply method (append params nil))))))))
-
-;; (setq server-endpoint (jsonrpc-connect
-;; "Emacs RPC client" '("localhost" 9393)
-;; (lambda (endpoint method id &rest params)
-;; (message "server wants to %s" method))))
+;; (setq server-server
+;; (make-network-process
+;; :name "Emacs RPC server" :server t :host "localhost" :service 44444
+;; :log (lambda (_server client _message)
+;; (jsonrpc-connect
+;; (process-name client)
+;; (make-instance 'jsonrpc-process-connection :process client)
+;; (lambda (endpoint method id params)
+;; (unless (memq method '(+ - * / vconcat append sit-for))
+;; (signal 'jsonrpc-error `((jsonrpc-error-message
+;; . "Sorry, this isn't allowed")
+;; (jsonrpc-error-code . -32601))))
+;; (jsonrpc-reply endpoint id :result
+;; (apply method (append params nil))))))))
;;
-;; ;; returns 3
-;; (jsonrpc-request server-endpoint '+ '(1 2))
-;; ;; errors with -32601
-;; (jsonrpc-request server-endpoint 'delete-directory "~/tmp")
-;; ;; signals an -32603 JSONRPC error
-;; (jsonrpc-request server-endpoint '+ '(a 2))
-;; ;; times out
-;; (jsonrpc-request server-endpoint 'sit-for '(5))
-;; ;; stretching it, but works
-;; (jsonrpc-request server-endpoint 'vconcat '([1 2 3] [3 4 5]))
-;; ;; json.el can't serialize this, json.el errors and request isn't sent
-;; (jsonrpc-request server-endpoint 'append '((1 2 3) (3 4 5)))
+;; (setq server-endpoint (jsonrpc-connect
+;; "Emacs RPC client" '("localhost" 9393)
+;; (lambda (endpoint method id &rest params)
+;; (message "server wants to %s" method))))
+;;
+;; ;; returns 3
+;; (jsonrpc-request server-endpoint '+ '(1 2))
+;; ;; errors with -32601
+;; (jsonrpc-request server-endpoint 'delete-directory "~/tmp")
+;; ;; signals an -32603 JSONRPC error
+;; (jsonrpc-request server-endpoint '+ '(a 2))
+;; ;; times out
+;; (jsonrpc-request server-endpoint 'sit-for '(5))
+;; ;; stretching it, but works
+;; (jsonrpc-request server-endpoint 'vconcat '([1 2 3] [3 4 5]))
+;; ;; json.el can't serialize this, json.el errors and request isn't sent
+;; (jsonrpc-request server-endpoint 'append '((1 2 3) (3 4 5)))
;;
;;; Code:
@@ -132,6 +133,7 @@
(require 'subr-x)
(require 'warnings)
(require 'pcase)
+(require 'ert)
(require 'array) ; xor
(defvar jsonrpc-find-connection-functions nil
@@ -169,58 +171,44 @@ FORMAT as the message."
:warning)))
(defclass jsonrpc-connection ()
- ((name :accessor jsonrpc-name
- :documentation "A name for the connection")
- (-dispatcher :accessor jsonrpc--dispatcher
- :documentation "Emacs-lisp function for server-invoked
methods.")
- (status :initform `(:unknown nil) :accessor jsonrpc-status
- :documentation "Status as declared by the server.
-A list (WHAT SERIOUS-P).")
- (-request-continuations :initform (make-hash-table)
- :accessor jsonrpc--request-continuations
- :documentation "A hash table of request ID to
continuation lambdas.")
- (-server-request-ids :accessor jsonrpc--server-request-ids
- :documentation "Server-initiated request id that
client hasn't replied to.")
- (-events-buffer :accessor jsonrpc--events-buffer
- :documentation "A buffer pretty-printing the JSON-RPC RPC
events")
- (contact :accessor jsonrpc-contact
- :documentation "Method used to contact a server.")
- (-on-shutdown :accessor jsonrpc--on-shutdown :documentation
- "Function run when JSONRPC server is dying.")
- (-deferred-actions :initform (make-hash-table :test #'equal)
- :accessor jsonrpc--deferred-actions
- :documentation "Actions deferred to when server is
thought to be ready.")))
+ ((name
+ :accessor jsonrpc-name
+ :documentation "A name for the connection")
+ (-dispatcher
+ :accessor jsonrpc--dispatcher
+ :documentation "Emacs-lisp function for server-invoked methods.")
+ (status
+ :initform `(:unknown nil) :accessor jsonrpc-status
+ :documentation "Status (WHAT SERIOUS-P) as declared by the server.")
+ (-request-continuations
+ :initform (make-hash-table)
+ :accessor jsonrpc--request-continuations
+ :documentation "A hash table of request ID to continuation lambdas.")
+ (-server-request-ids
+ :accessor jsonrpc--server-request-ids
+ :documentation "Server-initiated request ids that client hasn't replied
to.")
+ (-events-buffer
+ :accessor jsonrpc--events-buffer
+ :documentation "A buffer pretty-printing the JSON-RPC RPC events")
+ (contact
+ :accessor jsonrpc-contact
+ :documentation "Method used to contact a server.")
+ (-on-shutdown
+ :accessor jsonrpc--on-shutdown
+ :documentation "Function run when JSONRPC server is dying.")
+ (-deferred-actions
+ :initform (make-hash-table :test #'equal)
+ :accessor jsonrpc--deferred-actions
+ :documentation "Actions deferred to when server is thought to be ready.")))
(defclass jsonrpc-process-connection (jsonrpc-connection)
- ((-process :initarg :process :accessor jsonrpc--process
- :documentation "Process object wrapped by the this connection.")
- (-expected-bytes :accessor jsonrpc--expected-bytes
- :documentation "How many bytes declared by server")))
-
-(defun jsonrpc--make-process-connection (name contact)
- "Make a `jsonrpc-process-connection' from NAME and CONTACT."
- (let* ((readable-name (format "JSON-RPC server (%s)" name)
)
- (buffer (get-buffer-create (format "*%s output*" 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
- :connection-type 'pipe
- :coding 'no-conversion
- :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 #'jsonrpc--process-filter)
- (set-process-sentinel proc #'jsonrpc--process-sentinel)
- (with-current-buffer buffer
- (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t) proc))
- (let ((connection (make-instance 'jsonrpc-process-connection :process
proc)))
- (prog1 connection
- (process-put proc 'jsonrpc-connection connection)))))
+ ((-process
+ :initarg :process :accessor jsonrpc--process
+ ;; :initform (error "`:process' is a required initarg") ; doesn't work
+ :documentation "Process object wrapped by the this connection.")
+ (-expected-bytes
+ :accessor jsonrpc--expected-bytes
+ :documentation "How many bytes declared by server")))
(defmacro jsonrpc-obj (&rest what)
"Make WHAT a suitable argument for `json-encode'."
@@ -231,25 +219,31 @@ A list (WHAT SERIOUS-P).")
;;;###autoload
(cl-defun jsonrpc-connect (name contact dispatcher &optional on-shutdown)
- "Connect to JSONRPC endpoint hereafter known as NAME through CONTACT.
+ "Connect to JSONRPC endpoint NAME through CONTACT.
+
+This function creates an object (subprocess or network
+connection) wrapped in a `jsonrpc-process-connection' object.
NAME is a string naming the connection.
-CONTACT specifies how to connect. In the most generic case, it is
-a symbol naming a subclass of `jsonrpc-connection' or a
-previously created object of this type.
-
-However, for convenience, and when working with
-socket-and-stdio-based JSONRPC connections, it can also be a list
-of strings (COMMAND ARGS...) specifying how to start a server
-subconnection to connect to. Moreover, if the second element in
-the list is an integer number instead of a string, the list is
-interpreted as (HOST PORT PARAMETERS...) and a TCP connection is
-attempted to HOST on PORT, with the remaining PARAMETERS are
-given to `open-network-stream's optional arguments.
-
-CONTACT can also be a live connected process object. In that
-case its buffer, filter and sentinel are overwritten by
+In the most common case CONTACT is a list of strings (COMMAND
+ARGS...) specifying how to locally start a server subprocess to
+talk to via JSONRPC. If the second element in the list is an
+integer number instead of a string, the list is interpreted
+as (HOST PORT PARAMETERS...) and an attempt is made to contact
+HOST on PORT, with the remaining PARAMETERS are given to
+`open-network-stream's optional arguments.
+
+Moreover, if in either of these cases the first element in the
+list is a symbol, that symbol is taken to name a subclass of
+`jsonrpc-process-connection' which is used to create the object
+returned by this function. The remaining arguments are processed
+as described in the previous paragraph.
+
+CONTACT can also be a an object of the type
+`jsonrpc-process-connection' (or a subclass thereof) containing a
+pre-connected process object. In that case the processes buffer,
+filter and sentinel are henceforth overwritten and managed by
`jsonrpc-connect'.
ON-SHUTDOWN, if non-nil, is a function called on server exit and
@@ -269,16 +263,43 @@ signals an error with alist elements
`jsonrpc-error-message' and
`jsonrpc-error-code' in its DATA, the corresponding elements are
used for the automated error reply.
-If successful, `jsonrpc-connect' returns a `jsonrpc-connection'
-object representing the remote endpoint."
- (let* ((connection
- (cond ((cl-typep contact 'jsonrpc-connection)
- contact)
- ((symbolp contact)
- (make-instance contact))
- ((or (listp contact) (processp contact))
- (jsonrpc--make-process-connection name contact)))))
- (setf (jsonrpc-contact connection) contact
+If successful, `jsonrpc-connect' returns a
+`jsonrpc-process-connection' object representing the remote
+endpoint."
+ (let* ((readable-name (format "JSON-RPC server (%s)" name))
+ (buffer (get-buffer-create (format "*%s output*" readable-name)))
+ (original-contact contact)
+ (connection
+ (cond
+ ((cl-typep contact 'jsonrpc-process-connection)
+ (unless (process-live-p (jsonrpc--process contact))
+ (error "%s doesn't have a live process" contact))
+ contact)
+ ((listp contact)
+ (make-instance
+ (if (symbolp (car contact))
+ (prog1 (car contact) (setq contact (cdr contact)))
+ 'jsonrpc-process-connection)
+ :process
+ (cond ((integerp (cadr contact))
+ (apply #'open-network-stream readable-name buffer contact))
+ (t
+ (make-process :name readable-name
+ :command contact
+ :connection-type 'pipe
+ :coding 'no-conversion
+ :stderr (get-buffer-create
+ (format "*%s stderr*" name)))))))))
+ (proc (jsonrpc--process connection)))
+ (set-process-buffer proc buffer)
+ (set-marker (process-mark proc) (with-current-buffer buffer (point-min)))
+ (set-process-filter proc #'jsonrpc--process-filter)
+ (set-process-sentinel proc #'jsonrpc--process-sentinel)
+ (with-current-buffer buffer
+ (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t) proc))
+ (process-put proc 'jsonrpc-connection connection)
+ (setf (jsonrpc--process connection) proc
+ (jsonrpc-contact connection) original-contact
(jsonrpc-name connection) name
(jsonrpc--dispatcher connection) dispatcher
(jsonrpc--on-shutdown connection) (or on-shutdown #'ignore))
@@ -305,7 +326,7 @@ object representing the remote endpoint."
(jsonrpc--request-continuations connection))
(jsonrpc-message "Server exited with status %s" (process-exit-status
proc))
(unwind-protect
- (funcall (jsonrpc--on-shutdown connection) proc))
+ (funcall (jsonrpc--on-shutdown connection) connection))
(when (process-live-p proc)
(jsonrpc-warn "Brutally deleting non-compliant %s"
(jsonrpc-name connection))
@@ -595,7 +616,7 @@ TIMEOUT is nil)."
(list later (setq timer (funcall make-timer)))
(jsonrpc--deferred-actions connection))
;; Non-local exit!
- (cl-return-from jsonrpc-async-request-1 (list nil timer))))))
+ (cl-return-from jsonrpc--async-request-1 (list nil timer))))))
;; Really send it
;;
(jsonrpc-connection-send connection (jsonrpc-obj :jsonrpc "2.0"
- [elpa] externals/eglot cef3c29 22/69: Heroically merge master into jsonrpc-refactor (using imerge), (continued)
- [elpa] externals/eglot cef3c29 22/69: Heroically merge master into jsonrpc-refactor (using imerge), João Távora, 2018/06/22
- [elpa] externals/eglot a4441c6 37/69: Merge master into jsonrpc-refactor (using imerge), João Távora, 2018/06/22
- [elpa] externals/eglot 0e44b27 27/69: jsonrpc.el uses classes and generic functions, João Távora, 2018/06/22
- [elpa] externals/eglot 856a224 62/69: Simplify jsonrpc-connection-send, João Távora, 2018/06/22
- [elpa] externals/eglot 1f09fd3 59/69: Review commentary section before another review cycle, João Távora, 2018/06/22
- [elpa] externals/eglot 8fda30c 67/69: Merge master into jsonrpc-refactor (using imerge), João Távora, 2018/06/22
- [elpa] externals/eglot 7f4e273 31/69: Merge master into jsonrpc-refactor (using imerge), João Távora, 2018/06/22
- [elpa] externals/eglot 4525eca 43/69: Support json.c. API purely based on classes, João Távora, 2018/06/22
- [elpa] externals/eglot bb60c0c 21/69: Rename jrpc.el to jsonrpc.el, João Távora, 2018/06/22
- [elpa] externals/eglot 46e6107 54/69: Reshuffle definitions inside jsonrpc.el, João Távora, 2018/06/22
- [elpa] externals/eglot 6f1ecc6 28/69: Merge branch use-eieio-server-defclass into jsonrpc-refactor,
João Távora <=
- [elpa] externals/eglot 10559a5 56/69: Shuffle definitions around again, João Távora, 2018/06/22
- [elpa] externals/eglot b3c8b59 02/69: Refactor JSON-RPC lib jrpc.el from eglot.el, João Távora, 2018/06/22
- [elpa] externals/eglot 1ec47fb 51/69: Remove connection grabbing antics from jsonrpc.el, João Távora, 2018/06/22
- [elpa] externals/eglot f385d9c 69/69: Merge branch 'jsonrpc-refactor', bump version to 1.0, João Távora, 2018/06/22