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

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

[elpa] externals/eglot 4354710 01/16: Implement TCP autostart/autoconnec


From: João Távora
Subject: [elpa] externals/eglot 4354710 01/16: Implement TCP autostart/autoconnect (and support Ruby's Solargraph)
Date: Mon, 9 Jul 2018 17:27:12 -0400 (EDT)

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

    Implement TCP autostart/autoconnect (and support Ruby's Solargraph)
    
    * README.md (Installation and usage): Mention support for
    Solargraph
    (Connecting via TCP): New section
    (Connecting automatically): New section
    
    * eglot.el (eglot-server-programs): Add ruby-mode.
    Overhaul docstring.
    (eglot-lsp-server): Add inferior-process slot.
    (eglot--on-shutdown): Kill any autostarted inferior-process
    (eglot--guess-contact): Allow prompting with :autoport parameter.
    (eglot--connect): Consider :autoport case.
    (eglot--inferior-bootstrap): New helper.
---
 README.md |  36 +++++++++++++++++-
 eglot.el  | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++-----------
 2 files changed, 137 insertions(+), 23 deletions(-)

diff --git a/README.md b/README.md
index d3379d9..ecba0fe 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ for the language of your choice. Otherwise, it prompts you to 
enter one:
 * Javascript's [javascript-typescript-stdio][javascript-typescript-langserver]
 * Rust's [rls][rls]
 * Python's [pyls][pyls]
+* Ruby's [solargraph][solargraph]
 * Bash's [bash-language-server][bash-language-server]
 * PHP's [php-language-server][php-language-server]
 * [cquery][cquery] for C/C++
@@ -42,15 +43,46 @@ Let me know how well it works and we can add it to the 
list.  You can
 also enter a `server:port` pattern to connect to an LSP server. To
 skip the guess and always be prompted use `C-u M-x eglot`.
 
+## Connecting automatically
+
 You can also do:
 
 ```lisp
   (add-hook 'foo-mode-hook 'eglot-ensure)
 ```
 
-To attempt to start an eglot session automatically everytime a
+, to attempt to start an eglot session automatically everytime a
 `foo-mode` buffer is visited.
 
+## Connecting via TCP
+
+The examples above use a "pipe" to talk to the server, which works
+fine on Linux and OSX but in some cases
+[*may not work on Windows*][windows-subprocess-hang].
+
+To circumvent this limitation, or if the server doesn't like pipes,
+you can use `C-u M-x eglot` and give it `server:port` pattern to
+connect to a previously started TCP server serving LSP information.
+
+If you don't want to start it manually every time, you can configure
+Eglot to start it and immediately connect to it.  Ruby's
+[solargraph][solargraph] server already works this way out-of-the-box.
+
+For another example, suppose you also wanted start Python's `pyls`
+this way:
+
+```lisp
+(add-to-list 'eglot-server-programs
+             `(python-mode . ("pyls" "-v" "--tcp" "--host"
+                              "localhost" "--port" :autoport))))
+```
+
+You can see that the element associated with `python-mode` is now a
+more complicated invocation of the `pyls` program, which requests that
+it be started as a server.  Notice the `:autoport` symbol in there: it
+is replaced dynamically by a local port believed to be vacant, so that
+the ensuing TCP connection finds a listening server.
+
 # Commands and keybindings
 
 Here's a summary of available commands:
@@ -236,6 +268,8 @@ Under the hood:
 [php-language-server]: https://github.com/felixfbecker/php-language-server
 [company-mode]: https://github.com/company-mode/company-mode
 [cquery]: https://github.com/cquery-project/cquery
+[solargraph]: https://github.com/castwide/solargraph
+[windows-subprocess-hang]: 
https://www.gnu.org/software/emacs/manual/html_node/efaq-w32/Subprocess-hang.html
 
 
    
diff --git a/eglot.el b/eglot.el
index 7a4468d..6d0541a 100644
--- a/eglot.el
+++ b/eglot.el
@@ -85,32 +85,42 @@
                                 (sh-mode . ("bash-language-server" "start"))
                                 ((c++-mode
                                   c-mode) . (eglot-cquery "cquery"))
+                                (ruby-mode
+                                 . ("solagraph" "socket" "--port"
+                                    :autoport))
                                 (php-mode . ("php" "vendor/felixfbecker/\
 language-server/bin/php-language-server.php")))
   "How the command `eglot' guesses the server to start.
 An association list of (MAJOR-MODE . CONTACT) pairs.  MAJOR-MODE
 is a mode symbol, or a list of mode symbols.  The associated
-CONTACT specifies how to start a server for managing buffers of
-those modes.  CONTACT can be:
+CONTACT specifies how to connect to a server for managing buffers
+of those modes.  CONTACT can be:
 
 * In the most common case, a list of strings (PROGRAM [ARGS...]).
-PROGRAM is called with ARGS and is expected to serve LSP requests
-over the standard input/output channels.
+  PROGRAM is called with ARGS and is expected to serve LSP requests
+  over the standard input/output channels.
 
-* A list (HOST PORT [ARGS...]) where HOST is a string and PORT is
-a positive integer number for connecting to a server via TCP.
-Remaining ARGS are passed to `open-network-stream' for upgrading
-the connection with encryption or other capabilities.
+* A list (HOST PORT [TCP-ARGS...]) where HOST is a string and PORT is
+  na positive integer number for connecting to a server via TCP.
+  Remaining ARGS are passed to `open-network-stream' for
+  upgrading the connection with encryption or other capabilities.
+
+* A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereby a
+  combination of the two previous options is used..  First, an
+  attempt is made to find an available server port, then PROGRAM
+  is launched with ARGS; the `:autoport' keyword substituted for
+  that number; and MOREARGS.  Eglot then attempts to to establish
+  a TCP connection to that port number on the localhost.
 
 * A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol
-designating a subclass of `eglot-lsp-server', for representing
-experimental LSP servers.  INITARGS is a keyword-value plist used
-to initialize CLASS-NAME, or a plain list interpreted as the
-previous descriptions of CONTACT, in which case it is converted
-to produce a plist with a suitable :PROCESS initarg to
-CLASS-NAME.  The class `eglot-lsp-server' descends
-`jsonrpc-process-connection', which you should see for semantics
-of the mandatory :PROCESS argument.")
+  designating a subclass of `eglot-lsp-server', for representing
+  experimental LSP servers.  INITARGS is a keyword-value plist
+  used to initialize CLASS-NAME, or a plain list interpreted as
+  the previous descriptions of CONTACT, in which case it is
+  converted to produce a plist with a suitable :PROCESS initarg
+  to CLASS-NAME.  The class `eglot-lsp-server' descends
+  `jsonrpc-process-connection', which you should see for the
+  semantics of the mandatory :PROCESS argument.")
 
 (defface eglot-mode-line
   '((t (:inherit font-lock-constant-face :weight bold)))
@@ -205,8 +215,11 @@ lasted more than that many seconds."
     :documentation "List of buffers managed by server."
     :accessor eglot--managed-buffers)
    (saved-initargs
-    :documentation "Saved initargs for reconnection purposes"
-    :accessor eglot--saved-initargs))
+    :documentation "Saved initargs for reconnection purposes."
+    :accessor eglot--saved-initargs)
+   (inferior-process
+    :documentation "Server subprocess started automatically."
+    :accessor eglot--inferior-process))
   :documentation
   "Represents a server. Wraps a process for LSP communication.")
 
@@ -244,6 +257,9 @@ Don't leave this function with the server still running."
   (maphash (lambda (_id watches)
              (mapcar #'file-notify-rm-watch watches))
            (eglot--file-watches server))
+  ;; Kill any autostarted inferior processes
+  (when-let (proc (eglot--inferior-process server))
+    (delete-process proc))
   ;; Sever the project/server relationship for `server'
   (setf (gethash (eglot--project server) eglot--servers-by-project)
         (delq server
@@ -297,7 +313,11 @@ be guessed."
          (program (and (listp guess) (stringp (car guess)) (car guess)))
          (base-prompt
           (and interactive
-               "[eglot] Enter program to execute (or <host>:<port>): "))
+               "Enter program to execute (or <host>:<port>): "))
+         (program-guess
+          (and program
+               (combine-and-quote-strings (cl-subst ":autoport:"
+                                                    :autoport guess))))
          (prompt
           (and base-prompt
                (cond (current-prefix-arg base-prompt)
@@ -306,20 +326,22 @@ be guessed."
                               managed-mode base-prompt))
                      ((and program (not (executable-find program)))
                       (concat (format "[eglot] I guess you want to run `%s'"
-                                      (combine-and-quote-strings guess))
+                                      program-guess)
                               (format ", but I can't find `%s' in PATH!" 
program)
                               "\n" base-prompt)))))
          (contact
           (or (and prompt
                    (let ((s (read-shell-command
                              prompt
-                             (if program (combine-and-quote-strings guess))
+                             program-guess
                              'eglot-command-history)))
                      (if (string-match "^\\([^\s\t]+\\):\\([[:digit:]]+\\)$"
                                        (string-trim s))
                          (list (match-string 1 s)
                                (string-to-number (match-string 2 s)))
-                       (split-string-and-unquote s))))
+                       (cl-subst
+                        :autoport ":autoport:" (split-string-and-unquote s)
+                        :test #'equal))))
               guess
               (eglot--error "Couldn't guess for `%s'!" managed-mode))))
     (list managed-mode project class contact)))
@@ -429,6 +451,7 @@ This docstring appeases checkdoc, that's all."
   (let* ((nickname (file-name-base (directory-file-name
                                     (car (project-roots project)))))
          (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode))
+         autostart-inferior-process
          (initargs
           (cond ((keywordp (car contact)) contact)
                 ((integerp (cadr contact))
@@ -437,6 +460,14 @@ This docstring appeases checkdoc, that's all."
                                       readable-name nil
                                       (car contact) (cadr contact)
                                       (cddr contact)))))
+                ((and (stringp (car contact)) (memq :autoport contact))
+                 `(:process ,(lambda ()
+                               (pcase-let ((`(,connection . ,inferior)
+                                            (eglot--inferior-bootstrap
+                                             readable-name
+                                             contact)))
+                                 (setq autostart-inferior-process inferior)
+                                 connection))))
                 ((stringp (car contact))
                  `(:process ,(lambda ()
                                (make-process
@@ -463,6 +494,7 @@ This docstring appeases checkdoc, that's all."
     (setf (eglot--project server) project)
     (setf (eglot--project-nickname server) nickname)
     (setf (eglot--major-mode server) managed-major-mode)
+    (setf (eglot--inferior-process server) autostart-inferior-process)
     (push server (gethash project eglot--servers-by-project))
     (run-hook-with-args 'eglot-connect-hook server)
     (unwind-protect
@@ -495,6 +527,54 @@ This docstring appeases checkdoc, that's all."
       (when (and (not success) (jsonrpc-running-p server))
         (eglot-shutdown server)))))
 
+(defun eglot--inferior-bootstrap (name contact &optional connect-args)
+  "Use CONTACT to start a server, then connect to it.
+Return a cons of two process objects (CONNECTION . INFERIOR).
+Name both based on NAME.
+CONNECT-ARGS are passed as additional arguments to
+`open-network-stream'."
+  (let* ((port-probe (make-network-process :name "eglot-port-probe-dummy"
+                                           :server t
+                                           :host "localhost"
+                                           :service 0))
+         (port-number (unwind-protect
+                          (process-contact port-probe :service)
+                        (delete-process port-probe)))
+         inferior connection)
+    (unwind-protect
+        (progn
+          (setq inferior
+                (make-process
+                 :name (format "autostart-inferior-%s" name)
+                 :stderr (format "*%s stderr*" name)
+                 :command (cl-subst
+                           (format "%s" port-number) :autoport contact)))
+          (setq connection
+                (cl-loop
+                 repeat 10 for i from 1
+                 do (accept-process-output nil 0.5)
+                 while (process-live-p inferior)
+                 do (eglot--message
+                     "Trying to connect to localhost and port %s (attempt %s)"
+                     port-number i)
+                 thereis (ignore-errors
+                           (apply #'open-network-stream
+                                  (format "autoconnect-%s" name)
+                                  nil
+                                  "localhost" port-number connect-args))))
+          (cons connection inferior))
+      (cond ((and (process-live-p connection)
+                  (process-live-p inferior))
+             (eglot--message "Done, connected to %s!" port-number))
+            (t
+             (when inferior (delete-process inferior))
+             (when connection (delete-process connection))
+             (eglot--error "Could not start and connect to server%s"
+                           (if inferior
+                               (format " started with %s"
+                                       (process-command inferior))
+                             "!")))))))
+
 
 ;;; Helpers (move these to API?)
 ;;;



reply via email to

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