From 3e5b7cce3f12e87e2f88e3c7b95ab3c2da3a16ac Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Fri, 1 May 2020 11:01:11 +0200 Subject: [PATCH 01/12] [eglot] Add support for eglot lsp client in emacs - 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. --- README.md | 3 +- modules/config/default/+evil-bindings.el | 58 ++++++++++--------- modules/lang/cc/config.el | 38 +++++++++++- modules/lang/cc/packages.el | 4 +- modules/lang/haskell/packages.el | 2 +- modules/lang/python/config.el | 8 ++- modules/lang/python/packages.el | 2 +- modules/lang/rust/config.el | 5 ++ modules/tools/debugger/config.el | 2 +- modules/tools/debugger/doctor.el | 4 ++ modules/tools/lsp/README.org | 32 +++++++++- modules/tools/lsp/autoload/common.el | 9 +++ modules/tools/lsp/autoload/eglot.el | 19 ++++++ .../lsp/{autoload.el => autoload/lsp-mode.el} | 6 +- modules/tools/lsp/config.el | 28 ++++++++- modules/tools/lsp/doctor.el | 3 + modules/tools/lsp/flycheck-eglot.el | 57 ++++++++++++++++++ modules/tools/lsp/packages.el | 15 +++-- 18 files changed, 248 insertions(+), 47 deletions(-) create mode 100644 modules/tools/debugger/doctor.el create mode 100644 modules/tools/lsp/autoload/common.el create mode 100644 modules/tools/lsp/autoload/eglot.el rename modules/tools/lsp/{autoload.el => autoload/lsp-mode.el} (94%) create mode 100644 modules/tools/lsp/doctor.el create mode 100644 modules/tools/lsp/flycheck-eglot.el diff --git a/README.md b/README.md index 8cdb2f3e8..2aa3a7ad1 100644 --- a/README.md +++ b/README.md @@ -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! @@ -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 diff --git a/modules/config/default/+evil-bindings.el b/modules/config/default/+evil-bindings.el index 74b90c0a0..0c61aac43 100644 --- a/modules/config/default/+evil-bindings.el +++ b/modules/config/default/+evil-bindings.el @@ -343,32 +343,38 @@ ;;; 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)) ;;; f --- file (:prefix-map ("f" . "file") diff --git a/modules/lang/cc/config.el b/modules/lang/cc/config.el index bab3c4cfa..33daf41a3 100644 --- a/modules/lang/cc/config.el +++ b/modules/lang/cc/config.el @@ -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 diff --git a/modules/lang/cc/packages.el b/modules/lang/cc/packages.el index 1beaa6b98..946fad51d 100644 --- a/modules/lang/cc/packages.el +++ b/modules/lang/cc/packages.el @@ -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) diff --git a/modules/lang/haskell/packages.el b/modules/lang/haskell/packages.el index 23cdb842d..3792537b6 100644 --- a/modules/lang/haskell/packages.el +++ b/modules/lang/haskell/packages.el @@ -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")) diff --git a/modules/lang/python/config.el b/modules/lang/python/config.el index 8b3073e38..948a26cb6 100644 --- a/modules/lang/python/config.el +++ b/modules/lang/python/config.el @@ -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") @@ -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)))) @@ -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 diff --git a/modules/lang/python/packages.el b/modules/lang/python/packages.el index 4788487d6..47d141c4d 100644 --- a/modules/lang/python/packages.el +++ b/modules/lang/python/packages.el @@ -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 diff --git a/modules/lang/rust/config.el b/modules/lang/rust/config.el index 8cdbf43ea..7bf982684 100644 --- a/modules/lang/rust/config.el +++ b/modules/lang/rust/config.el @@ -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") diff --git a/modules/tools/debugger/config.el b/modules/tools/debugger/config.el index 12e8eeda4..0d582162a 100644 --- a/modules/tools/debugger/config.el +++ b/modules/tools/debugger/config.el @@ -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 diff --git a/modules/tools/debugger/doctor.el b/modules/tools/debugger/doctor.el new file mode 100644 index 000000000..5583b0efd --- /dev/null +++ b/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")) diff --git a/modules/tools/lsp/README.org b/modules/tools/lsp/README.org index 8144e8138..6449d54a4 100644 --- a/modules/tools/lsp/README.org +++ b/modules/tools/lsp/README.org @@ -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 @@ -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 diff --git a/modules/tools/lsp/autoload/common.el b/modules/tools/lsp/autoload/common.el new file mode 100644 index 000000000..ecb11e99c --- /dev/null +++ b/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))) diff --git a/modules/tools/lsp/autoload/eglot.el b/modules/tools/lsp/autoload/eglot.el new file mode 100644 index 000000000..6ad41005d --- /dev/null +++ b/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)) diff --git a/modules/tools/lsp/autoload.el b/modules/tools/lsp/autoload/lsp-mode.el similarity index 94% rename from modules/tools/lsp/autoload.el rename to modules/tools/lsp/autoload/lsp-mode.el index ec2a90355..0f33155fa 100644 --- a/modules/tools/lsp/autoload.el +++ b/modules/tools/lsp/autoload/lsp-mode.el @@ -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) @@ -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'." diff --git a/modules/tools/lsp/config.el b/modules/tools/lsp/config.el index a8183e44e..b19805d1a 100644 --- a/modules/tools/lsp/config.el +++ b/modules/tools/lsp/config.el @@ -7,11 +7,34 @@ 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")) @@ -123,7 +146,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))) @@ -164,6 +187,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 @@ -185,10 +209,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) diff --git a/modules/tools/lsp/doctor.el b/modules/tools/lsp/doctor.el new file mode 100644 index 000000000..aa45cd310 --- /dev/null +++ b/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.") diff --git a/modules/tools/lsp/flycheck-eglot.el b/modules/tools/lsp/flycheck-eglot.el new file mode 100644 index 000000000..b1fcee9ce --- /dev/null +++ b/modules/tools/lsp/flycheck-eglot.el @@ -0,0 +1,57 @@ +;;; flycheck-eglot --- Hacky eglot support in flycheck -*- lexical-binding: t; -*- + +(require 'flycheck) + +;;; Code: +(defun flycheck-eglot--start (checker callback) + "Clean up errors when done. + +CHECKER is the checker (eglot). +CALLBACK is the function that we need to call when we are done, on all the errors." + (cl-labels + ((flymake-diag->flycheck-err + (diag) + (with-current-buffer (flymake--diag-buffer diag) + (flycheck-error-new-at-pos + (flymake--diag-beg diag) + (pcase (flymake--diag-type diag) + ('eglot-note 'info) + ('eglot-warning 'warning) + ('eglot-error 'error) + (_ (error "Unknown diagnostic type, %S" diag))) + (flymake--diag-text diag) + :end-pos (flymake--diag-end diag) + :checker checker + :buffer (current-buffer) + :filename (buffer-file-name))))) + ;; NOTE: Setting up eglot to automatically create flycheck errors for the buffer. + (eglot-flymake-backend (lambda (flymake-diags &rest _) + (funcall callback 'finished (mapcar #'flymake-diag->flycheck-err flymake-diags)))) + ;; NOTE: Forcefully trigger a check in the buffer (function name is confusing) + (flycheck-buffer))) + +(defun flycheck-eglot--available-p () + (bound-and-true-p eglot--managed-mode)) + +(flycheck-define-generic-checker 'eglot + "Report `eglot' diagnostics using `flycheck'." + :start #'flycheck-eglot--start + :predicate #'flycheck-eglot--available-p + :modes '(prog-mode text-mode)) + +(push 'eglot flycheck-checkers) + +(defun +doom/eglot-prefer-flycheck-h () + (when eglot--managed-mode + (when-let ((current-checker (flycheck-get-checker-for-buffer))) + (unless (equal current-checker 'eglot) + (flycheck-add-next-checker 'eglot current-checker))) + (flycheck-add-mode 'eglot major-mode) + (flycheck-mode 1) + (flymake-mode -1))) + +(add-hook 'eglot--managed-mode-hook #'+doom/eglot-prefer-flycheck-h) + +(provide 'flycheck-eglot) +;;; flycheck-eglot.el ends here + diff --git a/modules/tools/lsp/packages.el b/modules/tools/lsp/packages.el index d0ed445dd..b8117fa61 100644 --- a/modules/tools/lsp/packages.el +++ b/modules/tools/lsp/packages.el @@ -1,9 +1,12 @@ ;; -*- no-byte-compile: t; -*- ;;; tools/lsp/packages.el -(package! lsp-mode :pin "81d62d581b21d847783831e6e5ca9d3c63fe9a4d") -(package! lsp-ui :pin "271b47cb33f11915295911f7cf8575f8a82a5e1c") -(when (featurep! :completion ivy) - (package! lsp-ivy :pin "dce58b5509271bbedb53ba9d0278dcb563a43977")) -(when (featurep! :completion helm) - (package! helm-lsp :pin "6b5ce182d7c94c62b55b8f7d0c7e643b2c30e560")) +(if (featurep! +eglot) + (package! eglot :pin "d99a4478a9") + (progn + (package! lsp-mode :pin "81d62d581b21d847783831e6e5ca9d3c63fe9a4d") + (package! lsp-ui :pin "271b47cb33f11915295911f7cf8575f8a82a5e1c") + (when (featurep! :completion ivy) + (package! lsp-ivy :pin "dce58b5509271bbedb53ba9d0278dcb563a43977")) + (when (featurep! :completion helm) + (package! helm-lsp :pin "6b5ce182d7c94c62b55b8f7d0c7e643b2c30e560")))) From bde772103dbd46b24e6238ab162ecbad531dece1 Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Wed, 6 May 2020 23:52:43 +0200 Subject: [PATCH 02/12] [eglot] Add emacs bindings --- modules/config/default/+emacs-bindings.el | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/config/default/+emacs-bindings.el b/modules/config/default/+emacs-bindings.el index 05a5f526c..ff2009136 100644 --- a/modules/config/default/+emacs-bindings.el +++ b/modules/config/default/+emacs-bindings.el @@ -41,7 +41,7 @@ :desc "List errors" "x" #'flymake-show-diagnostics-buffer (:when (featurep! :checkers syntax) :desc "List errors" "x" #'flycheck-list-errors) - (:when (featurep! :tools lsp) + (:when (and (featurep! :tools lsp) (not (featurep! :tools lsp + eglot))) :desc "LSP Code actions" "a" #'lsp-execute-code-action :desc "LSP Organize imports" "i" #'lsp-organize-imports :desc "LSP Rename" "r" #'lsp-rename @@ -52,7 +52,13 @@ :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 symbol in any workspace" "J" #'helm-lsp-global-workspace-symbol)) + (: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)) ;;; f --- file (:prefix-map ("f" . "file") From 98b3153df205d7cb1a3937be8e781c25279887a0 Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Mon, 18 May 2020 19:43:01 +0200 Subject: [PATCH 03/12] [review] Fix emacs bindings --- modules/config/default/+emacs-bindings.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/config/default/+emacs-bindings.el b/modules/config/default/+emacs-bindings.el index ff2009136..646f44209 100644 --- a/modules/config/default/+emacs-bindings.el +++ b/modules/config/default/+emacs-bindings.el @@ -41,7 +41,7 @@ :desc "List errors" "x" #'flymake-show-diagnostics-buffer (:when (featurep! :checkers syntax) :desc "List errors" "x" #'flycheck-list-errors) - (:when (and (featurep! :tools lsp) (not (featurep! :tools lsp + eglot))) + (:when (and (featurep! :tools lsp) (not (featurep! :tools lsp +eglot))) :desc "LSP Code actions" "a" #'lsp-execute-code-action :desc "LSP Organize imports" "i" #'lsp-organize-imports :desc "LSP Rename" "r" #'lsp-rename From 004ca6cf40ba3363fd7121f8370b7a4e038f7ad6 Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Mon, 18 May 2020 19:43:20 +0200 Subject: [PATCH 04/12] [review] Address eglot-flymake-backend call readablity --- modules/tools/lsp/flycheck-eglot.el | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/tools/lsp/flycheck-eglot.el b/modules/tools/lsp/flycheck-eglot.el index b1fcee9ce..d9f174bf1 100644 --- a/modules/tools/lsp/flycheck-eglot.el +++ b/modules/tools/lsp/flycheck-eglot.el @@ -25,8 +25,11 @@ CALLBACK is the function that we need to call when we are done, on all the error :buffer (current-buffer) :filename (buffer-file-name))))) ;; NOTE: Setting up eglot to automatically create flycheck errors for the buffer. - (eglot-flymake-backend (lambda (flymake-diags &rest _) - (funcall callback 'finished (mapcar #'flymake-diag->flycheck-err flymake-diags)))) + (eglot-flymake-backend + (lambda (flymake-diags &rest _) + (funcall callback + 'finished + (mapcar #'flymake-diag->flycheck-err flymake-diags)))) ;; NOTE: Forcefully trigger a check in the buffer (function name is confusing) (flycheck-buffer))) From da1117e7f0828a8d6e024b1e1dd70a061de6026f Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Wed, 20 May 2020 08:53:16 +0200 Subject: [PATCH 05/12] [doc] Add +eglot flag in docs/modules --- docs/modules.org | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules.org b/docs/modules.org index 0f761ab1c..b40aa8668 100644 --- a/docs/modules.org +++ b/docs/modules.org @@ -169,7 +169,7 @@ Small modules that give Emacs access to external tools & services. + gist - TODO + [[file:../modules/tools/lookup/README.org][lookup]] =+dictionary +docsets +offline +xwidget= - Universal jump-to & documentation lookup backend -+ [[file:../modules/tools/lsp/README.org][lsp]] =+peek= - TODO ++ [[file:../modules/tools/lsp/README.org][lsp]] =+peek +eglot= - Installation and configuration of language server protocol client (lsp-mode or eglot) + macos - TODO + [[file:../modules/tools/magit/README.org][magit]] =+forge= - TODO + make - TODO From 6a163fd5c198bdbfa56154a0fac138f59dffa1da Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Thu, 28 May 2020 11:03:49 +0200 Subject: [PATCH 06/12] [review] create +lsp.el and +eglot.el files This means we can remove a lot of `:unless (featurep! +eglot)` --- modules/tools/lsp/+eglot.el | 23 ++++ modules/tools/lsp/+lsp.el | 183 +++++++++++++++++++++++++++++++ modules/tools/lsp/config.el | 213 +----------------------------------- 3 files changed, 209 insertions(+), 210 deletions(-) create mode 100644 modules/tools/lsp/+eglot.el create mode 100644 modules/tools/lsp/+lsp.el diff --git a/modules/tools/lsp/+eglot.el b/modules/tools/lsp/+eglot.el new file mode 100644 index 000000000..a8ee9ca96 --- /dev/null +++ b/modules/tools/lsp/+eglot.el @@ -0,0 +1,23 @@ +;;; tools/lsp/+eglot.el -*- lexical-binding: t; -*- + +(use-package! eglot + :commands (eglot-ensure 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))) diff --git a/modules/tools/lsp/+lsp.el b/modules/tools/lsp/+lsp.el new file mode 100644 index 000000000..5d271a66f --- /dev/null +++ b/modules/tools/lsp/+lsp.el @@ -0,0 +1,183 @@ +;;; tools/lsp/+lsp.el -*- lexical-binding: t; -*- + +(use-package! lsp-mode + :commands lsp-install-server + :init + (setq lsp-session-file (concat doom-etc-dir "lsp-session")) + ;; Auto-kill LSP server after last workspace buffer is killed. + (setq lsp-keep-workspace-alive nil) + ;; Let `flycheck-check-syntax-automatically' determine this. + (setq lsp-flycheck-live-reporting nil) + ;; For `lsp-clients' + (setq lsp-server-install-dir (concat doom-etc-dir "lsp/") + lsp-intelephense-storage-path (concat doom-cache-dir "lsp-intelephense/")) + + (when (featurep! :config default +bindings) + ;; Let doom bind the lsp keymap. + (setq lsp-keymap-prefix nil)) + + ;; Disable LSP's superfluous, expensive and/or debatably unnecessary features. + ;; Some servers implement these poorly. Better to just rely on Emacs' native + ;; mechanisms and make these opt-in. + (setq lsp-enable-folding nil + ;; HACK Fix #2911, until it is resolved upstream. Links come in + ;; asynchronously from the server, but lsp makes no effort to + ;; "select" the original buffer before laying them down, so they + ;; could be rendered in the wrong buffer (like the minibuffer). + lsp-enable-links nil + ;; Potentially slow + lsp-enable-file-watchers nil + lsp-enable-text-document-color nil + lsp-enable-semantic-highlighting nil + ;; Don't modify our code without our permission + lsp-enable-indentation nil + lsp-enable-on-type-formatting nil + ;; capf is the preferred completion mechanism for lsp-mode now + lsp-prefer-capf t) + + :config + (set-popup-rule! "^\\*lsp-help" :size 0.35 :quit t :select t) + (set-lookup-handlers! 'lsp-mode :async t + :documentation #'lsp-describe-thing-at-point + :definition #'lsp-find-definition + :implementations #'lsp-find-implementation + :type-definition #'lsp-find-type-definition + :references #'lsp-find-references) + + ;; TODO Lazy load these. They don't need to be loaded all at once unless the + ;; user uses `lsp-install-server'. + (when lsp-auto-configure + (mapc (lambda (package) (require package nil t)) + lsp-client-packages)) + + (defadvice! +lsp-init-a (&optional arg) + "Enable `lsp-mode' in the current buffer. + +Meant to gimp `lsp', which is too eager about installing LSP servers, or +prompting to do so, or complaining about no LSP servers, or initializing +lsp-ui-mode, company, yasnippet and flycheck. We want LSP to work only if the +server is present, and for server installation to be a deliberate act by the +end-user. Also, setting up these other packages are handled by their respective +modules. + +Also see: ++ `+lsp-init-company-h' (on `lsp-mode-hook') ++ `+lsp-init-flycheck-or-flymake-h' (on `lsp-mode-hook') + +This also logs the resolved project root, if found, so we know where we are." + :override #'lsp + (interactive "P") + (and (buffer-file-name (buffer-base-buffer)) + (require 'lsp-mode nil t) + (setq-local + lsp--buffer-workspaces + (or (lsp--try-open-in-library-workspace) + (lsp--try-project-root-workspaces + (equal arg '(4)) + (and arg (not (equal arg 1)))))) + ;; read-process-output-max is only available on recent + ;; development builds of Emacs 27 and above + (or (not (boundp 'read-process-output-max)) + (setq-local read-process-output-max (* 1024 1024))) + ;; REVIEW LSP causes a lot of allocations, with or without Emacs 27+'s + ;; native JSON library, so we up the GC threshold to stave off + ;; GC-induced slowdowns/freezes. + (setq-local gcmh-high-cons-threshold (* 2 gcmh-high-cons-threshold)) + (prog1 (lsp-mode 1) + (setq-local lsp-buffer-uri (lsp--buffer-uri)) + ;; Announce what project root we're using, for diagnostic purposes + (if-let (root (lsp--calculate-root (lsp-session) (buffer-file-name))) + (lsp--info "Guessed project root is %s" (abbreviate-file-name root)) + (lsp--info "Could not guess project root.")) + (lsp--info "Connected to %s." + (apply #'concat + (mapcar + (lambda (it) (format "[%s]" (lsp--workspace-print it))) + lsp--buffer-workspaces)))))) + + (when (featurep! :config default +bindings) + (dolist (leader-key (list doom-leader-key doom-leader-alt-key)) + (let ((lsp-keymap-prefix (concat leader-key " c l"))) + (lsp-enable-which-key-integration)))) + + (add-hook! 'lsp-mode-hook + (defun +lsp-init-company-h () + (if (not (bound-and-true-p company-mode)) + (add-hook 'company-mode-hook #'+lsp-init-company-h t t) + ;; Ensure `company-capf' is at the front of `company-backends' + (setq-local company-backends + (cons 'company-capf + (remq 'company-capf company-backends))) + (remove-hook 'company-mode-hook #'+lsp-init-company-h t))) + (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 + (let ((lsp-diagnostic-package + (if (require 'flycheck nil t) :flycheck :flymake))) + (+lsp-init-flycheck-or-flymake-h))) + ((or :flymake 't) + (lsp--flymake-setup)) + (:flycheck + (let ((old-checker flycheck-checker)) + (lsp-flycheck-enable) + ;; Ensure file/dir local `flycheck-checker' is respected + (when old-checker + (setq-local flycheck-checker old-checker) + (kill-local-variable 'flycheck-check-syntax-automatically))))))) + + (defvar +lsp--deferred-shutdown-timer nil) + (defadvice! +lsp-defer-server-shutdown-a (orig-fn &optional restart) + "Defer server shutdown for a few seconds. +This gives the user a chance to open other project files before the server is +auto-killed (which is a potentially expensive process)." + :around #'lsp--shutdown-workspace + (if (or lsp-keep-workspace-alive + restart + (null +lsp-defer-shutdown) + (= +lsp-defer-shutdown 0)) + (funcall orig-fn restart) + (when (timerp +lsp--deferred-shutdown-timer) + (cancel-timer +lsp--deferred-shutdown-timer)) + (setq +lsp--deferred-shutdown-timer + (run-at-time + (if (numberp +lsp-defer-shutdown) +lsp-defer-shutdown 3) + nil (lambda (workspace) + (let ((lsp--cur-workspace workspace)) + (unless (lsp--workspace-buffers lsp--cur-workspace) + (funcall orig-fn)))) + lsp--cur-workspace)))) + + ;; Don't prompt to restart LSP servers while quitting Emacs + (add-hook! 'kill-emacs-hook (setq lsp-restart 'ignore))) + + +(use-package! lsp-ui + :hook (lsp-mode . lsp-ui-mode) + :config + (setq lsp-ui-doc-max-height 8 + lsp-ui-doc-max-width 35 + lsp-ui-sideline-ignore-duplicate t + ;; lsp-ui-doc is redundant with and more invasive than + ;; `+lookup/documentation' + lsp-ui-doc-enable nil + ;; Don't show symbol definitions in the sideline. They are pretty noisy, + ;; and there is a bug preventing Flycheck errors from being shown (the + ;; errors flash briefly and then disappear). + lsp-ui-sideline-show-hover nil) + + (when (featurep! +peek) + (set-lookup-handlers! 'lsp-ui-mode :async t + :definition 'lsp-ui-peek-find-definitions + :implementations 'lsp-ui-peek-find-implementation + :references 'lsp-ui-peek-find-references))) + + +(use-package! helm-lsp + :when (featurep! :completion helm) + :commands helm-lsp-workspace-symbol helm-lsp-global-workspace-symbol) + + +(use-package! lsp-ivy + :when (featurep! :completion ivy) + :commands lsp-ivy-workspace-symbol lsp-ivy-global-workspace-symbol) diff --git a/modules/tools/lsp/config.el b/modules/tools/lsp/config.el index b19805d1a..753cf32f5 100644 --- a/modules/tools/lsp/config.el +++ b/modules/tools/lsp/config.el @@ -8,213 +8,6 @@ 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")) - ;; Auto-kill LSP server after last workspace buffer is killed. - (setq lsp-keep-workspace-alive nil) - ;; Let `flycheck-check-syntax-automatically' determine this. - (setq lsp-flycheck-live-reporting nil) - ;; For `lsp-clients' - (setq lsp-server-install-dir (concat doom-etc-dir "lsp/") - lsp-intelephense-storage-path (concat doom-cache-dir "lsp-intelephense/")) - - (when (featurep! :config default +bindings) - ;; Let doom bind the lsp keymap. - (setq lsp-keymap-prefix nil)) - - ;; Disable LSP's superfluous, expensive and/or debatably unnecessary features. - ;; Some servers implement these poorly. Better to just rely on Emacs' native - ;; mechanisms and make these opt-in. - (setq lsp-enable-folding nil - ;; HACK Fix #2911, until it is resolved upstream. Links come in - ;; asynchronously from the server, but lsp makes no effort to - ;; "select" the original buffer before laying them down, so they - ;; could be rendered in the wrong buffer (like the minibuffer). - lsp-enable-links nil - ;; Potentially slow - lsp-enable-file-watchers nil - lsp-enable-text-document-color nil - lsp-enable-semantic-highlighting nil - ;; Don't modify our code without our permission - lsp-enable-indentation nil - lsp-enable-on-type-formatting nil - ;; capf is the preferred completion mechanism for lsp-mode now - lsp-prefer-capf t) - - :config - (set-popup-rule! "^\\*lsp-help" :size 0.35 :quit t :select t) - (set-lookup-handlers! 'lsp-mode :async t - :documentation #'lsp-describe-thing-at-point - :definition #'lsp-find-definition - :implementations #'lsp-find-implementation - :type-definition #'lsp-find-type-definition - :references #'lsp-find-references) - - ;; TODO Lazy load these. They don't need to be loaded all at once unless the - ;; user uses `lsp-install-server'. - (when lsp-auto-configure - (mapc (lambda (package) (require package nil t)) - lsp-client-packages)) - - (defadvice! +lsp-init-a (&optional arg) - "Enable `lsp-mode' in the current buffer. - -Meant to gimp `lsp', which is too eager about installing LSP servers, or -prompting to do so, or complaining about no LSP servers, or initializing -lsp-ui-mode, company, yasnippet and flycheck. We want LSP to work only if the -server is present, and for server installation to be a deliberate act by the -end-user. Also, setting up these other packages are handled by their respective -modules. - -Also see: -+ `+lsp-init-company-h' (on `lsp-mode-hook') -+ `+lsp-init-flycheck-or-flymake-h' (on `lsp-mode-hook') - -This also logs the resolved project root, if found, so we know where we are." - :override #'lsp - (interactive "P") - (and (buffer-file-name (buffer-base-buffer)) - (require 'lsp-mode nil t) - (setq-local - lsp--buffer-workspaces - (or (lsp--try-open-in-library-workspace) - (lsp--try-project-root-workspaces - (equal arg '(4)) - (and arg (not (equal arg 1)))))) - ;; read-process-output-max is only available on recent - ;; development builds of Emacs 27 and above - (or (not (boundp 'read-process-output-max)) - (setq-local read-process-output-max (* 1024 1024))) - ;; REVIEW LSP causes a lot of allocations, with or without Emacs 27+'s - ;; native JSON library, so we up the GC threshold to stave off - ;; GC-induced slowdowns/freezes. - (setq-local gcmh-high-cons-threshold (* 2 gcmh-high-cons-threshold)) - (prog1 (lsp-mode 1) - (setq-local lsp-buffer-uri (lsp--buffer-uri)) - ;; Announce what project root we're using, for diagnostic purposes - (if-let (root (lsp--calculate-root (lsp-session) (buffer-file-name))) - (lsp--info "Guessed project root is %s" (abbreviate-file-name root)) - (lsp--info "Could not guess project root.")) - (lsp--info "Connected to %s." - (apply #'concat - (mapcar - (lambda (it) (format "[%s]" (lsp--workspace-print it))) - lsp--buffer-workspaces)))))) - - (when (featurep! :config default +bindings) - (dolist (leader-key (list doom-leader-key doom-leader-alt-key)) - (let ((lsp-keymap-prefix (concat leader-key " c l"))) - (lsp-enable-which-key-integration)))) - - (add-hook! 'lsp-mode-hook - (defun +lsp-init-company-h () - (if (not (bound-and-true-p company-mode)) - (add-hook 'company-mode-hook #'+lsp-init-company-h t t) - ;; Ensure `company-capf' is at the front of `company-backends' - (setq-local company-backends - (cons 'company-capf - (remq 'company-capf company-backends))) - (remove-hook 'company-mode-hook #'+lsp-init-company-h t))) - (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 - (let ((lsp-diagnostic-package - (if (require 'flycheck nil t) :flycheck :flymake))) - (+lsp-init-flycheck-or-flymake-h))) - ((or :flymake 't) - (lsp--flymake-setup)) - (:flycheck - (let ((old-checker flycheck-checker)) - (lsp-flycheck-enable) - ;; Ensure file/dir local `flycheck-checker' is respected - (when old-checker - (setq-local flycheck-checker old-checker) - (kill-local-variable 'flycheck-check-syntax-automatically))))))) - - (defvar +lsp--deferred-shutdown-timer nil) - (defadvice! +lsp-defer-server-shutdown-a (orig-fn &optional restart) - "Defer server shutdown for a few seconds. -This gives the user a chance to open other project files before the server is -auto-killed (which is a potentially expensive process)." - :around #'lsp--shutdown-workspace - (if (or lsp-keep-workspace-alive - restart - (null +lsp-defer-shutdown) - (= +lsp-defer-shutdown 0)) - (funcall orig-fn restart) - (when (timerp +lsp--deferred-shutdown-timer) - (cancel-timer +lsp--deferred-shutdown-timer)) - (setq +lsp--deferred-shutdown-timer - (run-at-time - (if (numberp +lsp-defer-shutdown) +lsp-defer-shutdown 3) - nil (lambda (workspace) - (let ((lsp--cur-workspace workspace)) - (unless (lsp--workspace-buffers lsp--cur-workspace) - (funcall orig-fn)))) - lsp--cur-workspace)))) - - ;; Don't prompt to restart LSP servers while quitting Emacs - (add-hook! 'kill-emacs-hook (setq lsp-restart 'ignore))) - - -(use-package! lsp-ui - :unless (featurep! +eglot) - :hook (lsp-mode . lsp-ui-mode) - :config - (setq lsp-ui-doc-max-height 8 - lsp-ui-doc-max-width 35 - lsp-ui-sideline-ignore-duplicate t - ;; lsp-ui-doc is redundant with and more invasive than - ;; `+lookup/documentation' - lsp-ui-doc-enable nil - ;; Don't show symbol definitions in the sideline. They are pretty noisy, - ;; and there is a bug preventing Flycheck errors from being shown (the - ;; errors flash briefly and then disappear). - lsp-ui-sideline-show-hover nil) - - (when (featurep! +peek) - (set-lookup-handlers! 'lsp-ui-mode :async t - :definition 'lsp-ui-peek-find-definitions - :implementations 'lsp-ui-peek-find-implementation - :references 'lsp-ui-peek-find-references))) - - -(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) +(if (featurep! +eglot) + (load! "+eglot.el") + (load! "+lsp.el")) From 718d9a269018c777bb1176e462bc057401c620df Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Thu, 28 May 2020 11:12:35 +0200 Subject: [PATCH 07/12] [review] Changes --- modules/config/default/+emacs-bindings.el | 2 +- modules/lang/cc/config.el | 67 ++++++++++--------- modules/tools/lsp/+eglot.el | 9 +-- .../lsp/{ => autoload}/flycheck-eglot.el | 8 +-- modules/tools/lsp/packages.el | 13 ++-- 5 files changed, 46 insertions(+), 53 deletions(-) rename modules/tools/lsp/{ => autoload}/flycheck-eglot.el (92%) diff --git a/modules/config/default/+emacs-bindings.el b/modules/config/default/+emacs-bindings.el index 646f44209..71e1d1241 100644 --- a/modules/config/default/+emacs-bindings.el +++ b/modules/config/default/+emacs-bindings.el @@ -58,7 +58,7 @@ :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 "LSP Find implementation" "J" #'eglot-find-implementation)) ;;; f --- file (:prefix-map ("f" . "file") diff --git a/modules/lang/cc/config.el b/modules/lang/cc/config.el index 33daf41a3..c8a5ee10e 100644 --- a/modules/lang/cc/config.el +++ b/modules/lang/cc/config.el @@ -237,41 +237,42 @@ If rtags or rdm aren't available, fail silently instead of throwing a breaking e (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. + ;; NOTE : This setting is untested yet + (after! eglot + ;; 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")))) + (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 (and (featurep! +lsp) (not (featurep! :tools lsp +eglot))) diff --git a/modules/tools/lsp/+eglot.el b/modules/tools/lsp/+eglot.el index a8ee9ca96..7c963bed6 100644 --- a/modules/tools/lsp/+eglot.el +++ b/modules/tools/lsp/+eglot.el @@ -14,10 +14,7 @@ :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)) + (after! flycheck + (load! "flycheck-eglot.el"))) (set-lookup-handlers! 'eglot--managed-mode - :documentation #'+eglot/documentation-lookup-handler - :definition '(xref-find-definitions :async t) - :references '(xref-find-references :async t))) + :documentation #'+eglot/documentation-lookup-handler)) diff --git a/modules/tools/lsp/flycheck-eglot.el b/modules/tools/lsp/autoload/flycheck-eglot.el similarity index 92% rename from modules/tools/lsp/flycheck-eglot.el rename to modules/tools/lsp/autoload/flycheck-eglot.el index d9f174bf1..db7a38e1b 100644 --- a/modules/tools/lsp/flycheck-eglot.el +++ b/modules/tools/lsp/autoload/flycheck-eglot.el @@ -1,7 +1,5 @@ ;;; flycheck-eglot --- Hacky eglot support in flycheck -*- lexical-binding: t; -*- -(require 'flycheck) - ;;; Code: (defun flycheck-eglot--start (checker callback) "Clean up errors when done. @@ -44,7 +42,7 @@ CALLBACK is the function that we need to call when we are done, on all the error (push 'eglot flycheck-checkers) -(defun +doom/eglot-prefer-flycheck-h () +(defun +doom-eglot-prefer-flycheck-h () (when eglot--managed-mode (when-let ((current-checker (flycheck-get-checker-for-buffer))) (unless (equal current-checker 'eglot) @@ -53,8 +51,6 @@ CALLBACK is the function that we need to call when we are done, on all the error (flycheck-mode 1) (flymake-mode -1))) -(add-hook 'eglot--managed-mode-hook #'+doom/eglot-prefer-flycheck-h) +(add-hook 'eglot--managed-mode-hook #'+doom-eglot-prefer-flycheck-h) -(provide 'flycheck-eglot) ;;; flycheck-eglot.el ends here - diff --git a/modules/tools/lsp/packages.el b/modules/tools/lsp/packages.el index b8117fa61..90b673c18 100644 --- a/modules/tools/lsp/packages.el +++ b/modules/tools/lsp/packages.el @@ -3,10 +3,9 @@ (if (featurep! +eglot) (package! eglot :pin "d99a4478a9") - (progn - (package! lsp-mode :pin "81d62d581b21d847783831e6e5ca9d3c63fe9a4d") - (package! lsp-ui :pin "271b47cb33f11915295911f7cf8575f8a82a5e1c") - (when (featurep! :completion ivy) - (package! lsp-ivy :pin "dce58b5509271bbedb53ba9d0278dcb563a43977")) - (when (featurep! :completion helm) - (package! helm-lsp :pin "6b5ce182d7c94c62b55b8f7d0c7e643b2c30e560")))) + (package! lsp-mode :pin "81d62d581b21d847783831e6e5ca9d3c63fe9a4d") + (package! lsp-ui :pin "271b47cb33f11915295911f7cf8575f8a82a5e1c") + (when (featurep! :completion ivy) + (package! lsp-ivy :pin "dce58b5509271bbedb53ba9d0278dcb563a43977")) + (when (featurep! :completion helm) + (package! helm-lsp :pin "6b5ce182d7c94c62b55b8f7d0c7e643b2c30e560"))) From f35768af86b8dd8322cc74823df7758d5a6f2bdb Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Thu, 28 May 2020 11:19:33 +0200 Subject: [PATCH 08/12] Fix loading of flycheck-eglot --- modules/tools/lsp/+eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tools/lsp/+eglot.el b/modules/tools/lsp/+eglot.el index 7c963bed6..5458007ed 100644 --- a/modules/tools/lsp/+eglot.el +++ b/modules/tools/lsp/+eglot.el @@ -15,6 +15,6 @@ (set-popup-rule! "^\\*eglot-help" :size 0.35 :quit t :select t) (when (featurep! :checkers syntax) (after! flycheck - (load! "flycheck-eglot.el"))) + (load! "autoload/flycheck-eglot.el"))) (set-lookup-handlers! 'eglot--managed-mode :documentation #'+eglot/documentation-lookup-handler)) From f9d30cbf909acf3424acfd62073ec3ff939951c6 Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Thu, 28 May 2020 11:49:53 +0200 Subject: [PATCH 09/12] Wrap 'eglot-server-programs usage in after! eglot --- modules/tools/lsp/autoload/eglot.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tools/lsp/autoload/eglot.el b/modules/tools/lsp/autoload/eglot.el index 6ad41005d..f9b647d5e 100644 --- a/modules/tools/lsp/autoload/eglot.el +++ b/modules/tools/lsp/autoload/eglot.el @@ -6,7 +6,7 @@ "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) + (after! eglot (add-to-list 'eglot-server-programs `(,mode . ,server-call)))) ;;;###autoload From c41ae8995badc3935b15fcef28d3161f931a5554 Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Thu, 28 May 2020 11:50:24 +0200 Subject: [PATCH 10/12] Remove redundant featurep! check --- modules/tools/lsp/+eglot.el | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/tools/lsp/+eglot.el b/modules/tools/lsp/+eglot.el index 5458007ed..671599c7a 100644 --- a/modules/tools/lsp/+eglot.el +++ b/modules/tools/lsp/+eglot.el @@ -2,7 +2,6 @@ (use-package! eglot :commands (eglot-ensure eglot) - :when (featurep! +eglot) :init (setq eglot-sync-connect 1 eglot-connect-timeout 10 From 0f5c5bb2889b12141e002bf8a4db626c216eb91b Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Thu, 28 May 2020 11:55:32 +0200 Subject: [PATCH 11/12] Move and map interactive eglot/c++ function --- modules/lang/cc/README.org | 13 ++++++++++++- modules/lang/cc/autoload.el | 28 ++++++++++++++++++++++++++++ modules/lang/cc/config.el | 35 +++++++---------------------------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/modules/lang/cc/README.org b/modules/lang/cc/README.org index 666f25f5d..b563ae6a3 100644 --- a/modules/lang/cc/README.org +++ b/modules/lang/cc/README.org @@ -16,6 +16,8 @@ - [[#configure][Configure]] - [[#project-compile-settings][Project compile settings]] - [[#known-issues-with-bear-on-macos][Known issues with bear on macOS]] +- [[#appendix][Appendix]] + - [[#eglot-specific-bindings][Eglot specific bindings]] * Description This module adds support for the C-family of languages: C, C++, and Objective-C. @@ -42,7 +44,7 @@ This module adds support for the C-family of languages: C, C++, and Objective-C. + [[https://github.com/jimhourihan/glsl-mode][glsl-mode]]* + [[https://github.com/guidoschmidt/company-glsl][company-glsl]]* + =+lsp= - + [[https://github.com/MaskRay/emacs-ccls][ccls]] + + [[https://github.com/MaskRay/emacs-ccls][ccls]] if =:tools lsp= has *no* =+eglot= flag + =-lsp= + [[https://github.com/Sarcasm/irony-mode][irony]] + [[https://github.com/ikirill/irony-eldoc][irony-eldoc]] @@ -173,3 +175,12 @@ bear gmake Additional info: + [[https://github.com/rizsotto/Bear/issues/158][Empty compilation database with compiler in /usr/local]] + [[https://github.com/rizsotto/Bear/issues/152][Workaround for 'Empty compilation database on OS X Captain]] + +* Appendix +** Eglot specific bindings +When using =+lsp= and =:tools (lsp +eglot)=, lsp-mode is replaced with eglot, +and an additional function to get inheritance type hierarchy is added +| Binding | Description | +|------------------------------+--------------------------------------------------| +| ~ c t~ | ~Display inheritance type hierarchy (upwards)~ | +| ~ c t~ | ~Display inheritance type hierarchy (downwards)~ | diff --git a/modules/lang/cc/autoload.el b/modules/lang/cc/autoload.el index 3cc5f3f82..a97484b08 100644 --- a/modules/lang/cc/autoload.el +++ b/modules/lang/cc/autoload.el @@ -122,6 +122,34 @@ simpler." #'rtags-imenu #'imenu))) +;; Eglot specific helper, courtesy of MaskRay +;;;###autoload +(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"))) ;; ;; Hooks diff --git a/modules/lang/cc/config.el b/modules/lang/cc/config.el index c8a5ee10e..5729d3536 100644 --- a/modules/lang/cc/config.el +++ b/modules/lang/cc/config.el @@ -237,6 +237,12 @@ If rtags or rdm aren't available, fail silently instead of throwing a breaking e (lsp!)))) (when (and (featurep! +lsp) (featurep! :tools lsp +eglot)) + ;; Map eglot specific helper + (map! :localleader + :after cc-mode + :map c++-mode-map + :n :desc "Show type inheritance hierarchy" "ct" #'eglot-ccls-inheritance-hierarchy) + ;; NOTE : This setting is untested yet (after! eglot ;; IS-MAC custom configuration @@ -245,34 +251,7 @@ If rtags or rdm aren't available, fail silently instead of throwing a breaking e ((: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"))))) + :resourceDir (string-trim (shell-command-to-string "clang -print-resource-dir"))))))))))) (use-package! ccls :when (and (featurep! +lsp) (not (featurep! :tools lsp +eglot))) From dc3c2f9e78b9e46905930c7a5be32a73e07daa9a Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Thu, 28 May 2020 13:13:00 +0200 Subject: [PATCH 12/12] [review] no extension in load! / rename interactive function --- modules/lang/cc/autoload.el | 2 +- modules/lang/cc/config.el | 2 +- modules/tools/lsp/+eglot.el | 2 +- modules/tools/lsp/config.el | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/lang/cc/autoload.el b/modules/lang/cc/autoload.el index a97484b08..fb2362f02 100644 --- a/modules/lang/cc/autoload.el +++ b/modules/lang/cc/autoload.el @@ -124,7 +124,7 @@ simpler." ;; Eglot specific helper, courtesy of MaskRay ;;;###autoload -(defun eglot-ccls-inheritance-hierarchy (&optional derived) +(defun +cc/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." diff --git a/modules/lang/cc/config.el b/modules/lang/cc/config.el index 5729d3536..f8df58814 100644 --- a/modules/lang/cc/config.el +++ b/modules/lang/cc/config.el @@ -241,7 +241,7 @@ If rtags or rdm aren't available, fail silently instead of throwing a breaking e (map! :localleader :after cc-mode :map c++-mode-map - :n :desc "Show type inheritance hierarchy" "ct" #'eglot-ccls-inheritance-hierarchy) + :n :desc "Show type inheritance hierarchy" "ct" #'+cc/eglot-ccls-inheritance-hierarchy) ;; NOTE : This setting is untested yet (after! eglot diff --git a/modules/tools/lsp/+eglot.el b/modules/tools/lsp/+eglot.el index 671599c7a..e1b8d7397 100644 --- a/modules/tools/lsp/+eglot.el +++ b/modules/tools/lsp/+eglot.el @@ -14,6 +14,6 @@ (set-popup-rule! "^\\*eglot-help" :size 0.35 :quit t :select t) (when (featurep! :checkers syntax) (after! flycheck - (load! "autoload/flycheck-eglot.el"))) + (load! "autoload/flycheck-eglot"))) (set-lookup-handlers! 'eglot--managed-mode :documentation #'+eglot/documentation-lookup-handler)) diff --git a/modules/tools/lsp/config.el b/modules/tools/lsp/config.el index 753cf32f5..431a2701c 100644 --- a/modules/tools/lsp/config.el +++ b/modules/tools/lsp/config.el @@ -9,5 +9,5 @@ working on that project after closing the last buffer.") ;; TODO : set eglot-events-buffer-size to nil in doom-debug-mode (if (featurep! +eglot) - (load! "+eglot.el") - (load! "+lsp.el")) + (load! "+eglot") + (load! "+lsp"))