From 18d462732946a2cb23f4d69b427d56b6f2d274e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Sat, 17 Sep 2022 01:28:52 +0100 Subject: [PATCH] Per #590: Rework README.md about workspace configuration again Also tweak eglot-show-workspace-configuration a bit. * README.md (Workspace configuration): Rework. * eglot.el (eglot-show-workspace-configuration): Rework. (eglot--workspace-configuration-plist): New helper. --- README.md | 103 ++++++++++++++++++++++++++++++++++++++++-------------- eglot.el | 43 +++++++++++++---------- 2 files changed, 102 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index a98cfb64..808c78b6 100644 --- a/README.md +++ b/README.md @@ -137,41 +137,83 @@ the ensuing TCP connection finds a listening server. ## Workspace configuration Many servers can guess good defaults and operate nicely -out-of-the-box, but some need to be configured via a special LSP -`workspace/configuration` RPC call to work at all. Additionally, you -may also want a particular server to operate differently across -different projects. +out-of-the-box, but some need to know project-specific settings, which +LSP calls "workspace configuration". -Per-project settings are realized with the Elisp variable -`eglot-workspace-configuration`. +These per-project settings are realized with the Elisp variable +`eglot-workspace-configuration`. + +This variable's value is sent over to the server: + +* initially, as a [`didChangeConfiguration` notification][did-change-configuration]; +* as the response to [configuration request][configuration-request] from the server. Before considering what to set the variable to, one must understand -_how_ to set it. `eglot-workspace-configuration` is a -"directory-local" variable, and setting its variable globally or -buffer-locally likely makes no sense. It should be set via -`.dir-locals.el` or equivalent mechanisms. +_how_ to set it. `eglot-workspace-configuration` is a ["directory +variable"][dir-locals-emacs-manual]. Setting it globally or +buffer-locally makes little sense. It is usually set via +`.dir-locals.el` or [special-purpose elisp +functions][dir-locals-emacs-manual]. + +#### Format + +The variable's value is an _association list_: + +``` +((SECTION-1 . PARAM-OBJECT-1) + ... + (SECTION-N . PARAM-OBJECT-N)) +``` + +`SECTION-N` is an Elisp keyword naming the parameter section +understood by the server. `PARAM-OBJECT-N` contains one or more +settings pertaining to that server. -The variable's value is an _association list_ of _parameter sections_ -to _parameter objects_. Names and formats of section and parameter -objects are server specific. +`PARAM-OBJECT-N` is an Elisp object serialized to JSON by +[`json-serialize`][json-serialize]. The recommended format used in +this manual's examples is a [plist][plist] of keyword-value pairs, +though `json-serialize` also accepts other formats. + +When experimenting with settings, one may use `M-x +eglot-show-workspace-configuration` to inspect/debug the definite JSON +value sent over to the server. This helper function works even before +actually connecting to the server. #### Simple `eglot-workspace-configuration` To make a particular Python project always enable Pyls's snippet -support, put a file named `.dir-locals.el` in the project's root: +support, you may put a file named `.dir-locals.el` in the project's +root: ```lisp ((python-mode . ((eglot-workspace-configuration - . ((:pyls . (:plugins (:jedi_completion (:include_params t))))))))) + . + ;; the value in the format described above starts here + ((:pyls . (:plugins (:jedi_completion (:include_params t + :fuzzy t) + :pylint (:enabled :json-false))))) + ;; and ends here + )))) ``` This tells Emacs that any `python-mode` buffers in that directory should have a particular value of `eglot-workspace-configuration`. -Here, the value in question associates section `:pyls` with parameters -`(:plugins (:jedi_completion (:include_params t)))`. The parameter -object is a plist converted to JSON before being sent to the server. +Here, the value in question associates a parameter section `:pyls` +with a parameter objct that is a plist of plists. It is converted to +JSON before being sent to the server: + +```json +{ + "pyls": { + "plugins": { + "jedi_completion": { "include_params": true, "fuzzy": true }, + "pylint": { "enabled": false } + } + } +} +``` #### Multiple servers in `eglot-workspace-configuration` @@ -182,20 +224,25 @@ a section for `go-mode`, the file's contents now become: ```lisp ((python-mode . ((eglot-workspace-configuration - . ((:pyls . (:plugins (:jedi_completion (:include_params t)))))))) + . ((:pyls . (:plugins (:jedi_completion (:include_params t + :fuzzy t) + :pylint (:enabled :json-false)))))))) (go-mode . ((eglot-workspace-configuration . ((:gopls . (:usePlaceholders t))))))) ``` Alternatively, as a matter of taste, you may choose this equivalent -setup, which sets the variables value in all all major modes of all -buffers of a given project. +setup. This sets the value in all major-modes inside the project: the +major-mode specification is unneeded because the LSP server will +retrieve only the parameter section it is interested in. ```lisp ((nil . ((eglot-workspace-configuration - . ((:pyls . (:plugins (:jedi_completion (:include_params t)))) + . ((:pyls . (:plugins (:jedi_completion (:include_params t + :fuzzy t) + :pylint (:enabled :json-false)))) (:gopls . (:usePlaceholders t))))))) ``` @@ -211,10 +258,10 @@ leverage per-directory local variables. Look for the functions If you need to determine the workspace configuration base on some dynamic context, make `eglot-workspace-configuration` a function. It -is passed the `eglot-lsp-server` instance and runs with -`default-directory` set to the root of your project. The function -should return a value of the same form as described in the previous -paragraphs. +is passed the `eglot-lsp-server` instance of the connected server (if +any) and runs with `default-directory` set to the root of your +project. The function should return a value of the same form as +described in the previous paragraphs. ## Handling quirky servers @@ -626,3 +673,7 @@ for the request form, and we'll send it to you. [copyright-assignment]: https://www.fsf.org/licensing/contributor-faq [legally-significant]: https://www.gnu.org/prep/maintain/html_node/Legally-Significant.html#Legally-Significant [dir-locals-emacs-manual]: https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html +[configuration-request]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration +[did-change-configuration]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeConfiguration +[json-serialize]: https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-JSON.html +[plist]: https://www.gnu.org/software/emacs/manual/html_node/elisp/Property-Lists.html diff --git a/eglot.el b/eglot.el index 91733a8d..f02ec730 100644 --- a/eglot.el +++ b/eglot.el @@ -58,7 +58,6 @@ ;;; Code: -(require 'json) (require 'imenu) (require 'cl-lib) (require 'project) @@ -2204,23 +2203,28 @@ SECTION should be a keyword or a string. VALUE is a plist or a primitive type converted to JSON. The value of this variable can also be a unary function of a -`eglot-lsp-server' instance, the server connection requesting the -configuration. It should return an alist of the format described -above.") +single argument, which will be a connected `eglot-lsp-server' +instance. The function runs with `default-directory' set to the +root of the current project. It should return an alist of the +format described above.") ;;;###autoload (put 'eglot-workspace-configuration 'safe-local-variable 'listp) -(defun eglot-show-configuration (server) - "Dump `eglot-workspace-configuration' as json for debugging." - (interactive (list (eglot--read-server "Server configuration" - (eglot-current-server)))) - (let ((conf (eglot--workspace-configuration server))) - (with-current-buffer (get-buffer-create " *eglot configuration*") +(defun eglot-show-workspace-configuration (&optional server) + "Dump `eglot-workspace-configuration' as JSON for debugging." + (interactive (list (and (eglot-current-server) + (eglot--read-server "Server configuration" + (eglot-current-server))))) + (let ((conf (eglot--workspace-configuration-plist server))) + (with-current-buffer (get-buffer-create "*EGLOT workspace configuration*") (erase-buffer) (insert (jsonrpc--json-encode conf)) - (json-mode) - (json-pretty-print-buffer) + (with-no-warnings + (require 'json) + (require 'json-mode) + (json-mode) + (json-pretty-print-buffer)) (pop-to-buffer (current-buffer))))) (defun eglot--workspace-configuration (server) @@ -2228,6 +2232,14 @@ above.") (funcall eglot-workspace-configuration server) eglot-workspace-configuration)) +(defun eglot--workspace-configuration-plist (server) + "Returns `eglot-workspace-configuraiton' suitable serialization." + (or (cl-loop for (section . v) in (eglot--workspace-configuration server) + collect (if (keywordp section) section + (intern (format ":%s" section))) + collect v) + eglot--{})) + (defun eglot-signal-didChangeConfiguration (server) "Send a `:workspace/didChangeConfiguration' signal to SERVER. When called interactively, use the currently active server" @@ -2236,12 +2248,7 @@ When called interactively, use the currently active server" server :workspace/didChangeConfiguration (list :settings - (or (cl-loop for (section . v) in (eglot--workspace-configuration server) - collect (if (keywordp section) - section - (intern (format ":%s" section))) - collect v) - eglot--{})))) + (eglot--workspace-configuration-plist server)))) (cl-defmethod eglot-handle-request (server (_method (eql workspace/configuration)) &key items)