[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?)
;;;
- [elpa] externals/eglot 7467c15 11/16: Close #36: Inhibit auto-reconnect until connection is established, (continued)
- [elpa] externals/eglot 7467c15 11/16: Close #36: Inhibit auto-reconnect until connection is established, João Távora, 2018/07/09
- [elpa] externals/eglot a261a7b 10/16: Close #33: Bind default-directory when launching servers, João Távora, 2018/07/09
- [elpa] externals/eglot 543483a 13/16: Close #37: Unbreak completion when no possible annotation, João Távora, 2018/07/09
- [elpa] externals/eglot 1a61522 08/16: Close #32: Cache buffer's managing server, João Távora, 2018/07/09
- [elpa] externals/eglot 37296ed 14/16: Format documentation in completion annotations, João Távora, 2018/07/09
- [elpa] externals/eglot 45c651e 06/16: Per #31: Unbreak basic imenu functionality, João Távora, 2018/07/09
- [elpa] externals/eglot a99e129 03/16: Adjust timeout strategy when running tests, João Távora, 2018/07/09
- [elpa] externals/eglot 017fbdc 07/16: Close #31: Unbreak Imenu for cquery servers (and probably more), João Távora, 2018/07/09
- [elpa] externals/eglot 8219088 12/16: Close #34: Handle outrageously large and buggy line numbers, João Távora, 2018/07/09
- [elpa] externals/eglot 55ee8e9 15/16: * eglot.el (eglot-completion-at-point): Fix broken indentation, João Távora, 2018/07/09
- [elpa] externals/eglot 4354710 01/16: Implement TCP autostart/autoconnect (and support Ruby's Solargraph),
João Távora <=
- [elpa] externals/eglot 07b235f 16/16: jsonrpc.el is now a GNU ELPA depedency, João Távora, 2018/07/09