Skip to content

Commit d45aca0

Browse files
authored
Fix lsp-mode's tramp support (#4204)
* Fix lsp-mode's tramp support - This fixes the implementation of `lsp-mode` tramp support. After this PR the remote clients will be automatically registered and in most of the cases it will work out of the box. The remote connection is managed to a way similar to what eglot does. Fixes #4158 Fixes #4150 Fixes #4158 Fixes #4150 Fixes #3841 Fixes #3642 Fixes #3579 Fixes #3530 Fixes #3491 Fixes #3490 Fixes #3391 Fixes #3369 Fixes #3364 Fixes #3020 Fixes #3018 Fixes #3020 * Use executable-find with remote = t everywhere
1 parent 4f75f53 commit d45aca0

File tree

3 files changed

+78
-123
lines changed

3 files changed

+78
-123
lines changed

docs/manual-language-docs/lsp-rust-analyzer.md

-19
Original file line numberDiff line numberDiff line change
@@ -119,22 +119,3 @@ In the example below, first you see that:
119119

120120
This [unmerged PR](https://github.com/emacs-lsp/lsp-mode/pull/1740) contains an example method that allows
121121
modifying the signature that is displayed by eldoc.
122-
123-
### TRAMP Example
124-
125-
The following is an example configuration for using lsp-mode with a remote rust-analyzer server:
126-
127-
```
128-
(with-eval-after-load "lsp-rust"
129-
(lsp-register-client
130-
(make-lsp-client
131-
:new-connection (lsp-tramp-connection "rust-analyzer")
132-
:remote? t
133-
:major-modes '(rust-mode rustic-mode)
134-
:initialization-options 'lsp-rust-analyzer--make-init-options
135-
:notification-handlers (ht<-alist lsp-rust-notification-handlers)
136-
:action-handlers (ht ("rust-analyzer.runSingle" #'lsp-rust--analyzer-run-single))
137-
:library-folders-fn (lambda (_workspace) lsp-rust-analyzer-library-directories)
138-
:ignore-messages nil
139-
:server-id 'rust-analyzer-remote)))
140-
```

docs/page/remote.md

+2-21
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,14 @@ root_file: docs/page/remote.md
55

66
## TRAMP
77

8-
LSP mode has support for tramp buffers with the following requirements:
8+
`lsp-mode` has support for tramp buffers with the following requirements:
99

1010
- The language server has to be present on the remote server.
1111
- Having multi folder language server (like [Eclipse JDT LS](https://github.com/eclipse/eclipse.jdt.ls)) cannot have local and remote workspace folders.
1212

1313
### How does it work?
1414

15-
`lsp-mode` detects whether a particular file is located on remote machine and looks for a client which matches current file and it is marked as `:remote?` t. Then `lsp-mode` starts the client through tramp.
16-
17-
### Sample configuration
18-
19-
Here it is example how you can configure python language server to work when using `TRAMP`. Note that if you are trying to convert existing language server configuration you should copy all of it's properties(e. g. `:request-handlers`, `activation-fn`, etc). Also, when you are doing that you should make sure that none of the custom language server settings are not pointing to local path because those settings will be sent to the remote server.
20-
21-
```elisp
22-
(lsp-register-client
23-
(make-lsp-client :new-connection (lsp-tramp-connection "<binary name (e. g. pyls, rls)>")
24-
:major-modes '(python-mode)
25-
:remote? t
26-
:server-id 'pyls-remote))
27-
```
28-
29-
_Note:_ when you do not have root privileges on the remote machine to put the language server on the path you may alter the remote path by changing `tramp-remote-path`.
30-
31-
### Dealing with stderr
32-
33-
With TRAMP, Emacs does not have an easy way to distinguish stdout and stderr, so when the underlying LSP process writes to stderr, it breaks the `lsp-mode` parser. As a workaround, `lsp-mode` is redirecting stderr to `/tmp/<process-name>-<id>~stderr`.
34-
15+
`lsp-mode` detects whether a particular file is located on remote machine and looks for a client which matches current file and it is marked as `:remote?` t. Then `lsp-mode` starts the client through tramp. By default `lsp-mode` will copy the local client and mark it as `remote? t`. In most of the cases it is good enough but certain cases this may not work (e. g. if the server configuration contains references to local paths). In this case the user is supposed to create `.dir-local` configuration to override the references to local paths or open an issue on `lsp-mode` side to make the setting remote agnostic. To turn of automatic remote clients registration you can set `lsp-auto-register-remote-clients` to `nil`.
3516

3617
## Docker
3718

lsp-mode.el

+76-83
Original file line numberDiff line numberDiff line change
@@ -1145,7 +1145,7 @@ calling `remove-overlays'.")
11451145

11461146
(defvar-local lsp--virtual-buffer-point-max nil)
11471147

1148-
(cl-defgeneric lsp-execute-command (server command arguments)
1148+
(cl-defmethod lsp-execute-command (server command arguments)
11491149
"Ask SERVER to execute COMMAND with ARGUMENTS.")
11501150

11511151
(defun lsp-elt (sequence n)
@@ -5422,6 +5422,12 @@ In addition, each can have property:
54225422
:type 'boolean
54235423
:package-version '(lsp-mode . "8.0.1"))
54245424

5425+
(defcustom lsp-auto-register-remote-clients t
5426+
"When non-nil register remote when registering the local one."
5427+
:group 'lsp-mode
5428+
:type 'boolean
5429+
:package-version '(lsp-mode . "8.0.1"))
5430+
54255431
(defun lsp--display-inline-image (mode)
54265432
"Add image property if available."
54275433
(let ((plist-list (cdr (assq mode lsp--display-inline-image-alist))))
@@ -5524,7 +5530,7 @@ When language is nil render as markup if `markdown-mode' is loaded."
55245530
;; render the first line.
55255531
(_ (lsp-clients-extract-signature-on-hover contents nil))))
55265532

5527-
(cl-defgeneric lsp-clients-extract-signature-on-hover (contents _server-id)
5533+
(cl-defmethod lsp-clients-extract-signature-on-hover (contents _server-id)
55285534
"Extract a representative line from CONTENTS, to show in the echo area."
55295535
(car (s-lines (s-trim (lsp--render-element contents)))))
55305536

@@ -7285,28 +7291,29 @@ Return a nested alist keyed by symbol names. e.g.
72857291
(when menu-bar-mode
72867292
(lsp--imenu-refresh)))
72877293

7288-
(defun lsp-resolve-final-function (command)
7294+
(defun lsp-resolve-final-command (command &optional test?)
72897295
"Resolve final function COMMAND."
7290-
(-let [command (if (functionp command) (funcall command) command)]
7291-
(cl-etypecase command
7292-
(list
7293-
(cl-assert (seq-every-p (apply-partially #'stringp) command) nil
7294-
"Invalid command list")
7295-
command)
7296-
(string (list command)))))
7296+
(let* ((command (lsp-resolve-value command))
7297+
(command (cl-etypecase command
7298+
(list
7299+
(cl-assert (seq-every-p (apply-partially #'stringp) command) nil
7300+
"Invalid command list")
7301+
command)
7302+
(string (list command)))))
7303+
(if (and (file-remote-p default-directory) (not test?))
7304+
(list shell-file-name "-c"
7305+
(string-join (cons "stty raw > /dev/null;"
7306+
(mapcar #'shell-quote-argument command))
7307+
" "))
7308+
command)))
72977309

72987310
(defun lsp-server-present? (final-command)
72997311
"Check whether FINAL-COMMAND is present."
7300-
;; executable-find only gained support for remote checks after 27 release
7301-
(or (and (cond
7302-
((not (file-remote-p default-directory))
7303-
(executable-find (cl-first final-command)))
7304-
((version<= "27.0" emacs-version)
7305-
(with-no-warnings (executable-find (cl-first final-command) (file-remote-p default-directory))))
7306-
(t))
7307-
(prog1 t
7308-
(lsp-log "Command \"%s\" is present on the path." (s-join " " final-command))))
7309-
(ignore (lsp-log "Command \"%s\" is not present on the path." (s-join " " final-command)))))
7312+
(let ((binary-found? (executable-find (cl-first final-command) t)))
7313+
(if binary-found?
7314+
(lsp-log "Command \"%s\" is present on the path." (s-join " " final-command))
7315+
(lsp-log "Command \"%s\" is not present on the path." (s-join " " final-command)))
7316+
binary-found?))
73107317

73117318
(defun lsp--value-to-string (value)
73127319
"Convert VALUE to a string that can be set as value in an environment
@@ -7343,6 +7350,17 @@ corresponding to PATH, else returns `default-directory'."
73437350
(lsp-workspace-root path)
73447351
default-directory))
73457352

7353+
(defun lsp--fix-remote-cmd (program)
7354+
"Helper for `lsp-stdio-connection'.
7355+
Originally coppied from eglot."
7356+
7357+
(if (file-remote-p default-directory)
7358+
(list shell-file-name "-c"
7359+
(string-join (cons "stty raw > /dev/null;"
7360+
(mapcar #'shell-quote-argument program))
7361+
" "))
7362+
program))
7363+
73467364
(defun lsp-stdio-connection (command &optional test-command)
73477365
"Returns a connection property list using COMMAND.
73487366
COMMAND can be: A string, denoting the command to launch the
@@ -7362,26 +7380,28 @@ returned by COMMAND is available via `executable-find'"
73627380
(stringp el))
73637381
l))))))
73647382
(list :connect (lambda (filter sentinel name environment-fn workspace)
7365-
(if (functionp 'json-rpc-connection)
7366-
(lsp-json-rpc-connection
7367-
workspace
7368-
(lsp-resolve-final-function command))
7369-
(let ((final-command (lsp-resolve-final-function command))
7383+
(if (and (functionp 'json-rpc-connection)
7384+
(not (file-remote-p default-directory)))
7385+
(lsp-json-rpc-connection workspace (lsp-resolve-final-command command))
7386+
(let ((final-command (lsp-resolve-final-command command))
73707387
(process-name (generate-new-buffer-name name))
73717388
(process-environment
73727389
(lsp--compute-process-environment environment-fn)))
7373-
(let* ((stderr-buf (format "*%s::stderr*" process-name))
7390+
(let* ((stderr-buf (get-buffer-create (format "*%s::stderr*" process-name)))
73747391
(default-directory (lsp--default-directory-for-connection))
7392+
(tramp-use-ssh-controlmaster-options 'suppress)
7393+
(tramp-ssh-controlmaster-options "-o ControlMaster=no -o ControlPath=none")
73757394
(proc (make-process
73767395
:name process-name
73777396
:connection-type 'pipe
73787397
:buffer (format "*%s*" process-name)
7379-
:coding 'no-conversion
7398+
:coding 'utf-8-emacs-unix
73807399
:command final-command
73817400
:filter filter
73827401
:sentinel sentinel
73837402
:stderr stderr-buf
7384-
:noquery t)))
7403+
:noquery t
7404+
:file-handler t)))
73857405
(set-process-query-on-exit-flag proc nil)
73867406
(set-process-query-on-exit-flag (get-buffer-process stderr-buf) nil)
73877407
(with-current-buffer (get-buffer stderr-buf)
@@ -7390,7 +7410,8 @@ returned by COMMAND is available via `executable-find'"
73907410
(cons proc proc)))))
73917411
:test? (or
73927412
test-command
7393-
(lambda () (-> command lsp-resolve-final-function lsp-server-present?)))))
7413+
(lambda ()
7414+
(lsp-server-present? (lsp-resolve-final-command command t))))))
73947415

73957416
(defun lsp--open-network-stream (host port name)
73967417
"Open network stream to HOST:PORT.
@@ -7440,7 +7461,7 @@ process listening for TCP connections on the provided port."
74407461
(port (lsp--find-available-port host (cl-incf lsp--tcp-port)))
74417462
(command (funcall command-fn port))
74427463
(final-command (if (consp command) command (list command)))
7443-
(_ (unless (executable-find (cl-first final-command))
7464+
(_ (unless (lsp-server-present? final-command)
74447465
(user-error (format "Couldn't find executable %s" (cl-first final-command)))))
74457466
(process-environment
74467467
(lsp--compute-process-environment environment-fn))
@@ -7453,7 +7474,7 @@ process listening for TCP connections on the provided port."
74537474
(set-process-query-on-exit-flag tcp-proc nil)
74547475
(set-process-filter tcp-proc filter)
74557476
(cons tcp-proc proc)))
7456-
:test? (lambda () (executable-find (cl-first (funcall command-fn 0))))))
7477+
:test? (lambda () (lsp-server-present? (funcall command-fn 0)))))
74577478

74587479
(defalias 'lsp-tcp-server 'lsp-tcp-server-command)
74597480

@@ -7504,37 +7525,9 @@ should return the command to start the LS server."
75047525
(set-process-filter tcp-client-connection filter)
75057526
(set-process-sentinel tcp-client-connection sentinel)
75067527
(cons tcp-client-connection cmd-proc)))
7507-
:test? (lambda () (executable-find (cl-first (funcall command-fn 0))))))
7508-
7509-
(defun lsp-tramp-connection (local-command &optional generate-error-file-fn)
7510-
"Create LSP stdio connection named name.
7511-
LOCAL-COMMAND is either list of strings, string or function which
7512-
returns the command to execute."
7513-
(list :connect (lambda (filter sentinel name environment-fn _workspace)
7514-
(let* ((final-command (lsp-resolve-final-function local-command))
7515-
;; wrap with stty to disable converting \r to \n
7516-
(process-name (generate-new-buffer-name name))
7517-
(wrapped-command (s-join
7518-
" "
7519-
(append '("stty" "raw" ";")
7520-
final-command
7521-
(list
7522-
(concat "2>"
7523-
(or (when generate-error-file-fn
7524-
(funcall generate-error-file-fn name))
7525-
(format "/tmp/%s-%s-stderr" name
7526-
(cl-incf lsp--stderr-index))))))))
7527-
(process-environment
7528-
(lsp--compute-process-environment environment-fn)))
7529-
(let ((proc (start-file-process-shell-command process-name
7530-
(format "*%s*" process-name)
7531-
wrapped-command)))
7532-
(set-process-sentinel proc sentinel)
7533-
(set-process-filter proc filter)
7534-
(set-process-query-on-exit-flag proc nil)
7535-
(set-process-coding-system proc 'binary 'binary)
7536-
(cons proc proc))))
7537-
:test? (lambda () (-> local-command lsp-resolve-final-function lsp-server-present?))))
7528+
:test? (lambda () (lsp-server-present? (funcall command-fn 0)))))
7529+
7530+
(defalias 'lsp-tramp-connection 'lsp-stdio-connection)
75387531

75397532
(defun lsp--auto-configure ()
75407533
"Autoconfigure `company', `flycheck', `lsp-ui', etc if they are installed."
@@ -8066,7 +8059,7 @@ nil."
80668059
(if (and (f-absolute? path)
80678060
(f-exists? path))
80688061
path
8069-
(executable-find path))))
8062+
(executable-find path t))))
80708063

80718064
(defun lsp-package-path (dependency)
80728065
"Path to the DEPENDENCY each of the registered providers."
@@ -8099,7 +8092,8 @@ nil."
80998092
(f-join lsp-server-install-dir "npm" package
81008093
(cond ((eq system-type 'windows-nt) "")
81018094
(t "bin"))
8102-
path))))
8095+
path)
8096+
t)))
81038097
(unless (and path (f-exists? path))
81048098
(error "The package %s is not installed. Unable to find %s" package path))
81058099
path))
@@ -8362,10 +8356,7 @@ the next question until the queue is empty."
83628356
(->> lsp-clients hash-table-values (-filter pred)))
83638357

83648358
(defun lsp--find-clients ()
8365-
"Find clients which can handle BUFFER-MAJOR-MODE.
8366-
SESSION is the currently active session. The function will also
8367-
pick only remote enabled clients in case the FILE-NAME is on
8368-
remote machine and vice versa."
8359+
"Find clients which can handle current buffer."
83698360
(-when-let (matching-clients (lsp--filter-clients (-andfn #'lsp--supports-buffer?
83708361
#'lsp--server-binary-present?)))
83718362
(lsp-log "Found the following clients for %s: %s"
@@ -8398,23 +8389,25 @@ remote machine and vice versa."
83988389
(--each (lsp-session-folders (lsp-session))
83998390
(lsp-workspace-folders-remove it)))
84008391

8401-
84028392
(defun lsp-register-client (client)
84038393
"Registers LSP client CLIENT."
8404-
(cl-assert (symbolp (lsp--client-server-id client)) t)
8405-
(cl-assert (or
8406-
(functionp (lsp--client-activation-fn client))
8407-
(and (listp (lsp--client-major-modes client))
8408-
(seq-every-p (apply-partially #'symbolp)
8409-
(lsp--client-major-modes client))))
8410-
nil "Invalid activation-fn and/or major-modes.")
84118394
(let ((client-id (lsp--client-server-id client)))
84128395
(puthash client-id client lsp-clients)
84138396
(setplist (intern (format "lsp-%s-after-open-hook" client-id))
84148397
`( standard-value (nil) custom-type hook
84158398
custom-package-version (lsp-mode . "7.0.1")
84168399
variable-documentation ,(format "Hooks to run after `%s' server is run." client-id)
8417-
custom-requests nil))))
8400+
custom-requests nil)))
8401+
(when (and lsp-auto-register-remote-clients
8402+
(not (lsp--client-remote? client)))
8403+
(let ((remote-client (copy-lsp--client client)))
8404+
(setf (lsp--client-remote? remote-client) t
8405+
(lsp--client-server-id remote-client) (intern
8406+
(format "%s-tramp"
8407+
(lsp--client-server-id client)))
8408+
;; disable automatic download
8409+
(lsp--client-download-server-fn client) nil)
8410+
(lsp-register-client remote-client))))
84188411

84198412
(defun lsp--create-initialization-options (_session client)
84208413
"Create initialization-options from SESSION and CLIENT.
@@ -8581,24 +8574,24 @@ When ALL is t, erase all log buffers of the running session."
85818574

85828575

85838576

8584-
(cl-defgeneric lsp-process-id ((process process))
8577+
(cl-defmethod lsp-process-id ((process process))
85858578
(process-id process))
85868579

8587-
(cl-defgeneric lsp-process-name ((process process)) (process-name process))
8580+
(cl-defmethod lsp-process-name ((process process)) (process-name process))
85888581

8589-
(cl-defgeneric lsp-process-status ((process process)) (process-status process))
8582+
(cl-defmethod lsp-process-status ((process process)) (process-status process))
85908583

8591-
(cl-defgeneric lsp-process-kill ((process process))
8584+
(cl-defmethod lsp-process-kill ((process process))
85928585
(when (process-live-p process)
85938586
(kill-process process)))
85948587

8595-
(cl-defgeneric lsp-process-send ((process process) message)
8588+
(cl-defmethod lsp-process-send ((process process) message)
85968589
(condition-case err
85978590
(process-send-string process (lsp--make-message message))
85988591
('error (lsp--error "Sending to process failed with the following error: %s"
85998592
(error-message-string err)))))
86008593

8601-
(cl-defgeneric lsp-process-cleanup (process)
8594+
(cl-defmethod lsp-process-cleanup (process)
86028595
;; Kill standard error buffer only if the process exited normally.
86038596
;; Leave it intact otherwise for debugging purposes.
86048597
(let ((buffer (-> process process-name get-buffer)))

0 commit comments

Comments
 (0)