Skip to content

Commit

Permalink
[eglot] Add support for eglot lsp client in emacs
Browse files Browse the repository at this point in the history
- Update README
- Add eglot-specifics to cc, rs, py, hs
  removing unused lsp-mode packages when eglot is active
- Add eglot-specific bindings
- Add doctor warnings for debugger +lsp and +peek
- Add eglot-backed lookup-handlers
- Add flycheck checker using eglot for :checkers
  syntax users (using flycheck/flycheck#1676 and
  flycheck/flycheck#1592 discussion).
  This implementation is based on @marsam code, and uses recent
  Flycheck development in order to make the code smaller and
  easier to maintain.
  • Loading branch information
gagbo committed May 19, 2020
1 parent 1a0a757 commit 7fe662c
Show file tree
Hide file tree
Showing 18 changed files with 249 additions and 48 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -73,7 +73,7 @@ Check out [the FAQ][FAQ] for answers to common questions about the project.
- Optional vim emulation powered by [evil-mode], including ports of popular vim
plugins like [vim-sneak], [vim-easymotion], [vim-unimpaired] and
[more][ported-vim-plugins]!
- Opt-in LSP integration for many languages, using [lsp-mode].
- Opt-in LSP integration for many languages, using [lsp-mode] or [eglot]
- Support for *many* programming languages. Includes syntax highlighting,
linters/checker integration, inline code evaluation, code completion (where
possible), REPLs, documentation lookups, snippets, and more!
Expand Down Expand Up @@ -240,6 +240,7 @@ kind!
[helm]: https://github.com/emacs-helm/helm
[ivy]: https://github.com/abo-abo/swiper
[lsp-mode]: https://github.com/emacs-lsp/lsp-mode
[eglot]: https://github.com/joaotavora/eglot
[nix]: https://nixos.org
[ported-vim-plugins]: modules/editor/evil/README.org#ported-vim-plugins
[ripgrep]: https://github.com/BurntSushi/ripgrep
Expand Down
58 changes: 32 additions & 26 deletions modules/config/default/+evil-bindings.el
Expand Up @@ -345,32 +345,38 @@

;;; <leader> c --- code
(:prefix-map ("c" . "code")
:desc "Compile" "c" #'compile
:desc "Recompile" "C" #'recompile
:desc "Jump to definition" "d" #'+lookup/definition
:desc "Jump to references" "D" #'+lookup/references
:desc "Evaluate buffer/region" "e" #'+eval/buffer-or-region
:desc "Evaluate & replace region" "E" #'+eval:replace-region
:desc "Format buffer/region" "f" #'+format/region-or-buffer
(:when (featurep! :completion ivy)
:desc "Jump to symbol in current workspace" "j" #'lsp-ivy-workspace-symbol
:desc "Jump to symbol in any workspace" "J" #'lsp-ivy-global-workspace-symbol)
(:when (featurep! :completion helm)
:desc "Jump to symbol in current workspace" "j" #'helm-lsp-workspace-symbol
:desc "Jump to symbol in any workspace" "J" #'helm-lsp-global-workspace-symbol)
:desc "Jump to documentation" "k" #'+lookup/documentation
(:when (featurep! :tools lsp)
:desc "LSP Execute code action" "a" #'lsp-execute-code-action
:desc "LSP Organize imports" "i" #'lsp-organize-imports
:desc "LSP Rename" "r" #'lsp-rename
(:after lsp-mode
:desc "LSP" "l" lsp-command-map))
:desc "Send to repl" "s" #'+eval/send-region-to-repl
:desc "Delete trailing whitespace" "w" #'delete-trailing-whitespace
:desc "Delete trailing newlines" "W" #'doom/delete-trailing-newlines
:desc "List errors" "x" #'flymake-show-diagnostics-buffer
(:when (featurep! :checkers syntax)
:desc "List errors" "x" #'flycheck-list-errors))
(:unless (featurep! :tools lsp +eglot)
:desc "LSP Execute code action" "a" #'lsp-execute-code-action
:desc "LSP Organize imports" "i" #'lsp-organize-imports
(:when (featurep! :completion ivy)
:desc "Jump to symbol in current workspace" "j" #'lsp-ivy-workspace-symbol
:desc "Jump to symbol in any workspace" "J" #'lsp-ivy-global-workspace-symbol)
(:when (featurep! :completion helm)
:desc "Jump to symbol in current workspace" "j" #'helm-lsp-workspace-symbol
:desc "Jump to symbol in any workspace" "J" #'helm-lsp-global-workspace-symbol)
:desc "LSP Rename" "r" #'lsp-rename
(:after lsp-mode
:desc "LSP" "l" lsp-command-map))
(:when (featurep! :tools lsp +eglot)
:desc "LSP Execute code action" "a" #'eglot-code-actions
:desc "LSP Format buffer/region" "F" #'eglot-format
:desc "LSP Rename" "r" #'eglot-rename
:desc "LSP Find declaration" "j" #'eglot-find-declaration
:desc "LSP Find implementation" "J" #'eglot-find-implementation)
:desc "Compile" "c" #'compile
:desc "Recompile" "C" #'recompile
:desc "Jump to definition" "d" #'+lookup/definition
:desc "Jump to references" "D" #'+lookup/references
:desc "Evaluate buffer/region" "e" #'+eval/buffer-or-region
:desc "Evaluate & replace region" "E" #'+eval:replace-region
:desc "Format buffer/region" "f" #'+format/region-or-buffer
:desc "Jump to documentation" "k" #'+lookup/documentation
:desc "Send to repl" "s" #'+eval/send-region-to-repl
:desc "Delete trailing whitespace" "w" #'delete-trailing-whitespace
:desc "Delete trailing newlines" "W" #'doom/delete-trailing-newlines
:desc "List errors" "x" #'flymake-show-diagnostics-buffer
(:when (featurep! :checkers syntax)
:desc "List errors" "x" #'flycheck-list-errors))

;;; <leader> f --- file
(:prefix-map ("f" . "file")
Expand Down
38 changes: 37 additions & 1 deletion modules/lang/cc/config.el
Expand Up @@ -236,9 +236,45 @@ If rtags or rdm aren't available, fail silently instead of throwing a breaking e
(setq-local company-lsp-cache-candidates nil)
(lsp!))))

(when (and (featurep! +lsp) (featurep! :tools lsp +eglot))
;; TODO : test this value
;; IS-MAC custom configuration
(when IS-MAC
(add-to-list 'eglot-workspace-configuration
((:ccls . ((:clang . ,(list :extraArgs ["-isystem/Library/Developer/CommandLineTools/usr/include/c++/v1"
"-isystem/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include"
"-isystem/usr/local/include"]
:resourceDir (string-trim (shell-command-to-string "clang -print-resource-dir")))))))))
;; Eglot specific helper, courtesy of MaskRay
(defun eglot-ccls-inheritance-hierarchy (&optional derived)
"Show inheritance hierarchy for the thing at point.
If DERIVED is non-nil (interactively, with prefix argument), show
the children of class at point."
(interactive "P")
(if-let* ((res (jsonrpc-request
(eglot--current-server-or-lose)
:$ccls/inheritance
(append (eglot--TextDocumentPositionParams)
`(:derived ,(if derived t :json-false))
'(:levels 100) '(:hierarchy t))))
(tree (list (cons 0 res))))
(with-help-window "*ccls inheritance*"
(with-current-buffer standard-output
(while tree
(pcase-let ((`(,depth . ,node) (pop tree)))
(cl-destructuring-bind (&key uri range) (plist-get node :location)
(insert (make-string depth ?\ ) (plist-get node :name) "\n")
(make-text-button (+ (point-at-bol 0) depth) (point-at-eol 0)
'action (lambda (_arg)
(interactive)
(find-file (eglot--uri-to-path uri))
(goto-char (car (eglot--range-region range)))))
(cl-loop for child across (plist-get node :children)
do (push (cons (1+ depth) child) tree)))))))
(eglot--error "Hierarchy unavailable"))))

(use-package! ccls
:when (featurep! +lsp)
:when (and (featurep! +lsp) (not (featurep! :tools lsp +eglot)))
:after lsp
:init
(after! projectile
Expand Down
4 changes: 3 additions & 1 deletion modules/lang/cc/packages.el
Expand Up @@ -17,7 +17,9 @@
:pin "404cd0694a")))

(if (featurep! +lsp)
(package! ccls :pin "17ec7bb4cf")
(unless (featurep! :tools lsp +eglot)
;; ccls package is necessary only for lsp-mode.
(package! ccls :pin "17ec7bb4cf"))
(when (package! irony :pin "5f75fc0c92")
(package! irony-eldoc :pin "0df5831eaa")
(when (featurep! :checkers syntax)
Expand Down
2 changes: 1 addition & 1 deletion modules/lang/haskell/packages.el
Expand Up @@ -6,6 +6,6 @@
(when (featurep! +dante)
(package! dante :pin "4955bc7363")
(package! attrap :pin "4cf3e4a162"))
(when (or (featurep! +lsp)
(when (or (and (featurep! +lsp) (not (featurep! :tools lsp +eglot)))
(featurep! +ghcide))
(package! lsp-haskell :pin "582fa27c88"))
8 changes: 6 additions & 2 deletions modules/lang/python/config.el
Expand Up @@ -22,7 +22,10 @@ called.")
python-indent-guess-indent-offset-verbose nil)

(when (featurep! +lsp)
(add-hook 'python-mode-local-vars-hook #'lsp!))
(add-hook 'python-mode-local-vars-hook #'lsp!)
;; Use "mspyls" in eglot if in PATH
(when (executable-find "Microsoft.Python.LanguageServer")
(set-eglot-client! 'python-mode '("Microsoft.Python.LanguageServer"))))
:config
(set-repl-handler! 'python-mode #'+python/open-repl :persist t)
(set-docsets! 'python-mode "Python 3" "NumPy" "SciPy")
Expand Down Expand Up @@ -98,6 +101,7 @@ called.")
"Enable `anaconda-mode' if `lsp-mode' is absent and
`python-shell-interpreter' is present."
(unless (or (bound-and-true-p lsp-mode)
(bound-and-true-p eglot--managed-mode)
(bound-and-true-p lsp--buffer-deferred)
(not (executable-find python-shell-interpreter)))
(anaconda-mode +1))))
Expand Down Expand Up @@ -286,7 +290,7 @@ called.")


(use-package! lsp-python-ms
:when (featurep! +lsp)
:when (and (featurep! +lsp) (not (featurep! :tools lsp +eglot)))
:after lsp-clients
:preface
(after! python
Expand Down
2 changes: 1 addition & 1 deletion modules/lang/python/packages.el
Expand Up @@ -9,7 +9,7 @@
(package! flycheck-cython :pin "ecc4454d35")))

;; LSP
(when (featurep! +lsp)
(when (and (featurep! +lsp) (not (featurep! :tools lsp +eglot)))
(package! lsp-python-ms :pin "5d0c799099"))

;; Programming environment
Expand Down
5 changes: 5 additions & 0 deletions modules/lang/rust/config.el
Expand Up @@ -27,6 +27,11 @@
(after! rustic-flycheck
(add-to-list 'flycheck-checkers 'rustic-clippy)))

(when (featurep! +lsp)
(if (featurep! :tools lsp +eglot)
(setq rustic-lsp-client 'eglot)
(setq rustic-lsp-client 'lsp-mode)))

(map! :map rustic-mode-map
:localleader
(:prefix ("b" . "build")
Expand Down
2 changes: 1 addition & 1 deletion modules/tools/debugger/config.el
Expand Up @@ -89,7 +89,7 @@


(use-package! dap-mode
:when (featurep! +lsp)
:when (and (featurep! +lsp) (not (featurep! :tools lsp +eglot)))
:hook (dap-mode . dap-tooltip-mode)
:after lsp-mode
:demand t
Expand Down
4 changes: 4 additions & 0 deletions modules/tools/debugger/doctor.el
@@ -0,0 +1,4 @@
;;; tools/debugger/doctor.el -*- lexical-binding: t; -*-

(when (and (featurep! +lsp) (featurep! :tools lsp +eglot))
(warn! "+lsp flag is not compatible with :tools (lsp +eglot). Choose only one of (eglot or dap-mode) please"))
32 changes: 30 additions & 2 deletions modules/tools/lsp/README.org
Expand Up @@ -56,10 +56,16 @@ As of this writing, this is the state of LSP support in Doom Emacs:
** Module Flags
+ =+peek= Use =lsp-ui-peek= when looking up definitions and references with
functionality from the =:tools lookup= module.
+ =+eglot= Use [[https://elpa.gnu.org/packages/eglot.html][Eglot]] instead of [[https://github.com/emacs-lsp/lsp-mode][LSP-mode]] to implement the LSP client in
Emacs.

** Plugins
+ [[https://github.com/emacs-lsp/lsp-mode][lsp-mode]]
+ [[https://github.com/emacs-lsp/lsp-ui][lsp-ui]]
+ [[https://github.com/emacs-lsp/lsp-ivy][lsp-ivy]]
+ [[https://github.com/emacs-lsp/helm-lsp][helm-lsp]]
+ [[https://github.com/joaotavora/eglot][eglot]]

** Hacks
+ ~lsp-mode~ has been modified not to automatically install missing LSP servers.
This is done to adhere to our "Your system, your rules" mantra, which insist
Expand All @@ -75,15 +81,37 @@ You'll find a table that lists available language servers and how to install
them [[https://github.com/emacs-lsp/lsp-mode#supported-languages][in the lsp-mode project README]]. The documentation of the module for your
targeted language will contain brief instructions as well.

For eglot users, you can see the list of [[https://github.com/joaotavora/eglot/blob/master/README.md#connecting-to-a-server][default servers supported in the README]].
There is also instructions to add another server easily.

* TODO Features
** LSP-powered project search
When =:completion ivy= or =:completion helm= is active, LSP is used to search a
symbol indexed by the LSP server :
Without the =+eglot= flag, and when =:completion ivy= or =:completion helm= is
active, LSP is used to search a symbol indexed by the LSP server :
| Keybind | Description |
|-----------+-------------------------------------|
| =SPC c j= | Jump to symbol in current workspace |
| =SPC c J= | Jump to symbol in any workspace |
** Differences between eglot and lsp-mode
Entering the debate about which one to use would be useless. Doom provides an
easy way to switch out lsp client implementations so you can test for yourself
which one you prefer.

Mainly, from a code point of view, lsp-mode has a lot of custom code for UI
(=lsp-ui-peek=, =lsp-ui-sideline=, ...), while eglot is more barebones with a
closer integration with "more basic" emacs packages (=eldoc=, =xref=, ...).

* TODO Configuration

* TODO Troubleshooting
** My language server is not found
Check the entry in the [[../../../docs/faq.org][FAQ]] about "Doom can't find my executables/doesn't inherit
the correct ~PATH~"
** LSP/Eglot is not started automatically in my buffer
Make sure that you added the =+lsp= flag to the language you're using too in
your init.el :
#+BEGIN_SRC diff
:lang
-python
+(python +lsp)
#+END_SRC
9 changes: 9 additions & 0 deletions modules/tools/lsp/autoload/common.el
@@ -0,0 +1,9 @@
;;; tools/lsp/autoload/common.el -*- lexical-binding: t; -*-

;;;###autodef
(defun lsp! ()
"Dispatch to call the currently used lsp client entrypoint"
(interactive)
(if (featurep! +eglot)
(eglot-ensure)
(lsp-deferred)))
19 changes: 19 additions & 0 deletions modules/tools/lsp/autoload/eglot.el
@@ -0,0 +1,19 @@
;;; tools/lsp/autoload/eglot.el -*- lexical-binding: t; -*-
;;;###if (featurep! +eglot)

;;;###autodef
(defun set-eglot-client! (mode server-call)
"Add SERVER-CALL list as a possible lsp server for given major MODE.
Example : (set-eglot-client! 'python-mode `(,(concat doom-etc-dir \"lsp/mspyls/Microsoft.Python.LanguageServer\")))"
(when (featurep! +eglot)
(add-to-list 'eglot-server-programs `(,mode . ,server-call))))

;;;###autoload
(defun +eglot/documentation-lookup-handler ()
"Documentation lookup handler using eglot :document/hover handler.
Mostly a rewrite of `eglot-help-at-point', which should be used interactively."
(interactive)
(eglot-help-at-point)
(display-buffer eglot--help-buffer))
@@ -1,4 +1,5 @@
;;; feature/lsp/autoload.el -*- lexical-binding: t; -*-
;;; tools/lsp/autoload/lsp-mode.el -*- lexical-binding: t; -*-
;;;###if (not (featurep! +eglot))

;;;###autodef
(defun set-lsp-priority! (client priority)
Expand All @@ -9,9 +10,6 @@
priority)
(error "No LSP client named %S" client)))

;;;###autodef
(defalias 'lsp! #'lsp-deferred)

;;;###autoload
(defun +lsp/uninstall-server (dir)
"Delete a LSP server from `lsp-server-install-dir'."
Expand Down
30 changes: 28 additions & 2 deletions modules/tools/lsp/config.el
Expand Up @@ -7,15 +7,38 @@ workspace buffer is closed.
This delay prevents premature server shutdown when a user still intends on
working on that project after closing the last buffer.")

;; TODO : set eglot-events-buffer-size to nil in doom-debug-mode

;;
;;; Packages

(use-package! eglot
:when (featurep! +eglot)
:init
(setq eglot-sync-connect 1
eglot-connect-timeout 10
eglot-autoshutdown t
eglot-send-changes-idle-time 0.5
;; NOTE: Do NOT set eglot-auto-display-help-buffer to t.
;; With popup-rule! :select t, eglot will steal focus from the source code very often.
eglot-auto-display-help-buffer nil)
:config
(set-popup-rule! "^\\*eglot-help" :size 0.35 :quit t :select t)
(when (featurep! :checkers syntax)
;; Eager loading which is okay-ish since we want eglot to feed flycheck as soon as possible.
(load! "flycheck-eglot.el")
(require 'flycheck-eglot))
(set-lookup-handlers! 'eglot--managed-mode
:documentation #'+eglot/documentation-lookup-handler
:definition '(xref-find-definitions :async t)
:references '(xref-find-references :async t)))

(use-package! lsp-mode
:unless (featurep! +eglot)
:commands lsp-install-server
:init
(setq lsp-session-file (concat doom-etc-dir "lsp-session"))
;; Don't prompt the user for the project root every time we open a new
;; Do not prompt the user for the project root every time we open a new
;; lsp-worthy file, instead, try to guess it with projectile.
(setq lsp-auto-guess-root t)
;; Auto-kill LSP server after last workspace buffer is killed.
Expand Down Expand Up @@ -126,7 +149,7 @@ This also logs the resolved project root, if found, so we know where we are."
(defun +lsp-init-flycheck-or-flymake-h ()
"Set up flycheck-mode or flymake-mode, depending on `lsp-diagnostic-package'."
(pcase lsp-diagnostic-package
((or :auto 'nil) ; try flycheck, fall back to flymake
((or :auto 'nil) ; try flycheck, fall back to flymake
(let ((lsp-diagnostic-package
(if (require 'flycheck nil t) :flycheck :flymake)))
(+lsp-init-flycheck-or-flymake-h)))
Expand Down Expand Up @@ -179,6 +202,7 @@ auto-killed (which is a potentially expensive process)."


(use-package! lsp-ui
:unless (featurep! +eglot)
:hook (lsp-mode . lsp-ui-mode)
:config
(setq lsp-ui-doc-max-height 8
Expand All @@ -200,10 +224,12 @@ auto-killed (which is a potentially expensive process)."


(use-package! helm-lsp
:unless (featurep! +eglot)
:when (featurep! :completion helm)
:commands helm-lsp-workspace-symbol helm-lsp-global-workspace-symbol)


(use-package! lsp-ivy
:unless (featurep! +eglot)
:when (featurep! :completion ivy)
:commands lsp-ivy-workspace-symbol lsp-ivy-global-workspace-symbol)
3 changes: 3 additions & 0 deletions modules/tools/lsp/doctor.el
@@ -0,0 +1,3 @@
;;; tools/lsp/doctor.el -*- lexical-binding: t; -*-

(assert! (not (and (featurep! +eglot) (featurep! +peek))) "+eglot and +peek flags are not compatible. Peek uses lsp-mode, while Eglot is another package altogether for LSP.")

0 comments on commit 7fe662c

Please sign in to comment.