emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/eglot b3c8b59 02/69: Refactor JSON-RPC lib jrpc.el from


From: João Távora
Subject: [elpa] externals/eglot b3c8b59 02/69: Refactor JSON-RPC lib jrpc.el from eglot.el
Date: Fri, 22 Jun 2018 11:54:53 -0400 (EDT)

branch: externals/eglot
commit b3c8b59d4f5ed5470c28684f76431dd5c1882a47
Author: João Távora <address@hidden>
Commit: João Távora <address@hidden>

    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
---
 eglot.el | 836 +++++++++++++++++----------------------------------------------
 jrpc.el  | 502 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 727 insertions(+), 611 deletions(-)

diff --git a/eglot.el b/eglot.el
index 4cb3bec..934270c 100644
--- a/eglot.el
+++ b/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,114 +74,63 @@ 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 ()
-  "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
+(jrpc-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
+(jrpc-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
+(jrpc-define-process-var eglot--project nil
   "The project the server belongs to.")
 
-(eglot--define-process-var eglot--spinner `(nil nil t)
+(jrpc-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).")
+(jrpc-define-process-var eglot--moribund nil
+  "Non-nil if server is about to exit")
 
-(eglot--define-process-var eglot--inhibit-autoreconnect eglot-autoreconnect
+(jrpc-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 <host>:<port>")
-
-(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--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--project-short-name (project)
   "Give PROJECT a short name."
@@ -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)
diff --git a/jrpc.el b/jrpc.el
new file mode 100644
index 0000000..91ad0ea
--- /dev/null
+++ b/jrpc.el
@@ -0,0 +1,502 @@
+;;; jrpc.el --- JSON-RPC library                  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2018 Free Software Foundation, Inc.
+
+;; Author: João Távora <address@hidden>
+;; Maintainer: João Távora <address@hidden>
+;; URL: https://github.com/joaotavora/eglot
+;; Keywords: processes, languages, 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Originally extracted from eglot.el (Emacs LSP client)
+;;
+;;
+;; code        message meaning
+;; -32700      Parse error     Invalid JSON was received by the server.
+;; An error occurred on the server while parsing the JSON text.
+;; -32600      Invalid Request The JSON sent is not a valid Request object.
+;; -32601      Method not found        The method does not exist / is not 
available.
+;; -32602      Invalid params  Invalid method parameter(s).
+;; -32603      Internal error  Internal JSON-RPC error.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'json)
+(require 'subr-x)
+(require 'warnings)
+
+(defgroup jrpc nil
+  "Interaction with Language Server Protocol servers"
+  :prefix "jrpc-"
+  :group 'applications)
+
+(defcustom jrpc-request-timeout 10
+  "How many seconds to wait for a reply from the server."
+  :type :integer)
+
+(defvar jrpc-find-process-functions nil
+  "Special hook to find an active JSON-RPC process.")
+
+(defun jrpc-current-process ()
+  "The current logical JSON-RPC process."
+  (run-hook-with-args-until-success 'jrpc-find-process-functions))
+
+(defun jrpc-current-process-or-lose ()
+  "Return the current JSON-RPC process or error."
+  (or (jrpc-current-process)
+      (jrpc-error "No current JSON-RPC process")))
+
+(defun jrpc-error (format &rest args)
+  "Error out with FORMAT with ARGS."
+  (error (apply #'format format args)))
+
+(defun jrpc-message (format &rest args)
+  "Message out with FORMAT with ARGS."
+  (message (concat "[jrpc] " (apply #'format format args))))
+
+(defun jrpc-warn (format &rest args)
+  "Warning message with FORMAT and ARGS."
+  (apply #'jrpc-message (concat "(warning) " format) args)
+  (let ((warning-minimum-level :error))
+    (display-warning 'jrpc
+                     (apply #'format format args)
+                     :warning)))
+
+(defmacro jrpc-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))))
+
+(jrpc-define-process-var jrpc-name nil
+  "A name for the process")
+
+(jrpc-define-process-var jrpc--method-prefix nil
+  "Emacs-lisp function prefix for server-invoked methods.")
+
+(jrpc-define-process-var jrpc-status `(:unknown nil)
+  "Status as declared by the server.
+A list (WHAT SERIOUS-P).")
+
+(jrpc-define-process-var jrpc--expected-bytes nil
+  "How many bytes declared by server")
+
+(jrpc-define-process-var jrpc--pending-continuations (make-hash-table)
+  "A hash table of request ID to continuation lambdas")
+
+(jrpc-define-process-var jrpc--events-buffer nil
+  "A buffer pretty-printing the JSON-RPC RPC events")
+
+(jrpc-define-process-var jrpc-contact nil
+  "Method used to contact a server.")
+
+(jrpc-define-process-var jrpc--deferred-actions
+    (make-hash-table :test #'equal)
+  "Actions deferred to when server is thought to be ready.")
+
+(defun jrpc-outstanding-request-ids (proc)
+  "IDs of outstanding JSON-RPC requests for PROC."
+  (hash-table-keys (jrpc--pending-continuations proc)))
+
+(defun jrpc--make-process (name contact)
+  "Make a process from CONTACT.
+NAME is a name to give the inferior process or connection.
+CONTACT is as `jrpc-contact'.  Returns a process object."
+  (let* ((readable-name (format "JSON-RPC server (%s)" name)                   
                                         )
+         (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 #'jrpc--process-filter)
+    (set-process-sentinel proc #'jrpc--process-sentinel)
+    proc))
+
+(defmacro jrpc-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, i.e. is basically `list'
+  `(list ,@what))
+
+(cl-defun jrpc-connect (name contact prefix)
+  "Connect to JSON-RPC server hereafter known as NAME through CONTACT.
+
+NAME is a string naming the server.
+
+CONTACT is either a list of strings (a shell command and
+arguments), or a list of a single string of the form
+<host>:<port>.
+
+PREFIX specifies how the server-invoked methods find their Elisp
+counterpart. If a server invokes method \"FooBar\" and PREFIX is
+\"fancy-mode-\", then the function `fancy-mode-FooBar' will be
+called with arguments (PROCESS [JSON]). JSON is either a plist of
+key-value pairs or, for JSON arrays, a non-list sequence."
+  (let* ((proc (jrpc--make-process name contact))
+         (buffer (process-buffer proc)))
+    (setf (jrpc-contact proc) contact
+          (jrpc-name proc) name
+          (jrpc--method-prefix proc) prefix)
+    (with-current-buffer buffer
+      (let ((inhibit-read-only t))
+        (erase-buffer)
+        (read-only-mode t)
+        proc))))
+
+(defvar jrpc-server-moribund-hook nil
+  "Hook run when JSON-RPC server is dying.
+Run after running any error handlers for outstanding requests.
+Each hook function is passed the process object for the server.")
+
+(defun jrpc--process-sentinel (proc change)
+  "Called when PROC undergoes CHANGE."
+  (jrpc-log-event proc `(:message "Process state changed" :change ,change))
+  (when (not (process-live-p proc))
+    (with-current-buffer (jrpc-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)))
+             (jrpc--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"))))
+                 (jrpc--pending-continuations proc))
+      (jrpc-message "Server exited with status %s" (process-exit-status proc))
+      (run-hook-with-args 'jrpc-server-moribund-hook proc)
+      (delete-process proc))))
+
+(defun jrpc--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 (jrpc--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
+                                  (jrpc--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 (jrpc--expected-bytes proc) expected-bytes))))))
+
+(defun jrpc-events-buffer (process &optional interactive)
+  "Display events buffer for current LSP connection PROCESS.
+INTERACTIVE is t if called interactively."
+  (interactive (list (jrpc-current-process-or-lose) t))
+  (let* ((probe (jrpc--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 (jrpc--events-buffer process) buffer))
+                       buffer))))
+    (when interactive (display-buffer buffer))
+    buffer))
+
+(defun jrpc-log-event (proc message &optional type)
+  "Log an jrpc-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 (jrpc-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 jrpc--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 (jrpc--pending-continuations 
proc)))))
+      (jrpc-log-event proc message 'server)
+      (when error (setf (jrpc-status proc) `(,error t)))
+      (cond (method
+             ;; a server notification or a server request
+             (let* ((handler-sym (intern (concat (jrpc--method-prefix proc)
+                                                 method))))
+               (if (functionp handler-sym)
+                   (apply handler-sym proc (append
+                                            (plist-get message :params)
+                                            (if id `(:id ,id))))
+                 (jrpc-warn "No implementation of method %s yet" method)
+                 (when id
+                   (jrpc-reply
+                    proc id
+                    :error (jrpc-obj :code -32601
+                                     :message "Method unimplemented"))))))
+            (continuations
+             (cancel-timer (cl-third continuations))
+             (remhash id (jrpc--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
+             (jrpc-warn "Ooops no continuation for id %s" id)))
+      (jrpc--call-deferred proc)
+      (force-mode-line-update t))))
+
+(defun jrpc-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))
+    (jrpc-log-event proc message 'client)))
+
+(defvar jrpc--next-request-id 0)
+
+(defun jrpc--next-request-id ()
+  "Compute the next id for a client request."
+  (setq jrpc--next-request-id (1+ jrpc--next-request-id)))
+
+(defun jrpc-forget-pending-continuations (process)
+  "Stop waiting for responses from the current LSP PROCESS."
+  (interactive (list (jrpc-current-process-or-lose)))
+  (clrhash (jrpc--pending-continuations process)))
+
+(defun jrpc-clear-status (process)
+  "Clear most recent error message from PROCESS."
+  (interactive (list (jrpc-current-process-or-lose)))
+  (setf (jrpc-status process) nil))
+
+(defun jrpc--call-deferred (proc)
+  "Call PROC's deferred actions, who may again defer themselves."
+  (when-let ((actions (hash-table-values (jrpc--deferred-actions proc))))
+    (jrpc-log-event proc `(:running-deferred ,(length actions)))
+    (mapc #'funcall (mapcar #'car actions))))
+
+(defvar jrpc-ready-predicates '()
+  "Special hook of predicates controlling deferred actions.
+If one of these returns nil, a deferrable `jrpc-async-request'
+will be deferred.  Each predicate is passed the symbol for the
+request request and a process object.")
+
+(cl-defmacro jrpc-lambda (cl-lambda-list &body body)
+  (declare (indent 1) (debug (sexp &rest form)))
+  `(cl-function (lambda ,cl-lambda-list ,@body)))
+
+(cl-defun jrpc-async-request (proc
+                              method
+                              params
+                              &rest args
+                              &key success-fn error-fn timeout-fn
+                              (timeout jrpc-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 (jrpc--next-request-id))
+         (existing-timer nil)
+         (make-timeout
+          (lambda ( )
+            (or existing-timer
+                (run-with-timer
+                 timeout nil
+                 (lambda ()
+                   (remhash id (jrpc--pending-continuations proc))
+                   (funcall (or timeout-fn
+                                (lambda ()
+                                  (jrpc-error
+                                   "Tired of waiting for reply to %s, id=%s"
+                                   method id))))))))))
+    (when deferred
+      (let* ((buf (current-buffer))
+             (existing (gethash (list deferred buf) (jrpc--deferred-actions 
proc))))
+        (when existing (setq existing-timer (cadr existing)))
+        (if (run-hook-with-args-until-failure 'jrpc-ready-predicates
+                                              deferred proc)
+            (remhash (list deferred buf) (jrpc--deferred-actions proc))
+          (jrpc-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 #'jrpc-async-request proc
+                                                     method params args)))))))
+            (puthash (list deferred buf) (list later (funcall make-timeout))
+                     (jrpc--deferred-actions proc))
+            (cl-return-from jrpc-async-request nil)))))
+    ;; Really run it
+    ;;
+    (puthash id
+             (list (or success-fn
+                       (jrpc-lambda (&rest _ignored)
+                         (jrpc-log-event
+                          proc (jrpc-obj :message "success ignored" :id id))))
+                   (or error-fn
+                       (jrpc-lambda (&key code message &allow-other-keys)
+                         (setf (jrpc-status proc) `(,message t))
+                         proc (jrpc-obj :message "error ignored, status set"
+                                        :id id :error code)))
+                   (funcall make-timeout))
+             (jrpc--pending-continuations proc))
+    (jrpc-process-send proc (jrpc-obj :jsonrpc "2.0"
+                                      :id id
+                                      :method method
+                                      :params params))))
+
+(defun jrpc-request (proc method params &optional deferred)
+  "Like `jrpc-async-request' for PROC, METHOD and PARAMS, but synchronous.
+Meaning only return locally if successful, otherwise exit non-locally.
+DEFERRED is passed to `jrpc-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 `jrpc-ready-predicates'.
+  (let* ((tag (cl-gensym "jrpc-request-catch-tag"))
+         (retval
+          (catch tag
+            (jrpc-async-request
+             proc method params
+             :success-fn (lambda (&rest args)
+                           (throw tag
+                                  `(done ,(if (vectorp (car args))
+                                              (car args) args))))
+             :error-fn (jrpc-lambda (&key code message &allow-other-keys)
+                         (throw tag
+                                `(error ,(format "Oops: %s: %s" code 
message))))
+             :timeout-fn (lambda ()
+                           (throw tag
+                                  '(error "Timed out")))
+             :deferred deferred)
+            (while t (accept-process-output nil 30)))))
+    (when (eq 'error (car retval)) (jrpc-error (cadr retval)))
+    (cadr retval)))
+
+(cl-defun jrpc-notify (process method params)
+  "Notify PROCESS of something, don't expect a reply.e"
+  (jrpc-process-send process (jrpc-obj :jasonrpc  "2.0"
+                                       :method method
+                                       :params params)))
+
+(cl-defun jrpc-reply (process id &key result error)
+  "Reply to PROCESS's request ID with MESSAGE."
+  (jrpc-process-send
+   process `(:jasonrpc  "2.0" :id ,id
+                        ,@(when result `(:result ,result))
+                        ,@(when error `(:error ,error)))))
+
+(defun jrpc-mapply (fun seq)
+  "Apply FUN to every element of SEQ."
+  (mapcar (lambda (e) (apply fun e)) seq))
+
+(provide 'jrpc)
+;;; jrpc.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]