LSP's & Eglot

In this post I will share part of my set up when working in Python or Go using their LSP's and the Eglot client now already built in Emacs. The objective of this post is to provide a few pointers on how to set up your Emacs config and Eglot so that you can already get started with the starting features needed to develop in Go or Python.

After spending some time with lsp mode and some of its other relative packages (lsp-treemacs and lsp-ui), I have found that the set up worked great with smaller projects. Plus, I wanted to find a package that required less configuration and dependencies, so for these reasons, I decided to give eglot a try.

Eglot

Eglot is now an Emacs built in package that is more minimalist and lightweight than lsp-mode, it requires less configuration while providing support for many lsp's and, also, it works with other built-in Emacs tools like xref and eldoc.

Trying out eglot in a small project, you realize that the things it provides, while they are not as many as lsp-mode + lsp-treemacs etc… are actually all you need to navigate your project and have a good autocomplete experience. And, talking about autocomplete, you can use corfu or company mode without problems or even emacs native auto-complete. However, it should be kept in mind that on big codebases the performance loss would make editing a bit bothersome when compared to other IDE's like PyCharm.

Python & Go setup

All in all, the configuration I have can be summed up to relatively few lines. With these the experience should already be good for smaller projects.

First of all we can set up eglot and add the necessary Python lsp servers we intend to use:

(use-package eglot
  :ensure t
  :defer t
  :hook ((python-mode . eglot-ensure)
         (go-mode . eglot-ensure))
  :config
  (add-to-list 'eglot-server-programs
               `(python-mode
                 . ,(eglot-alternatives '(("pyright-langserver" "--stdio")
                                          "jedi-language-server"
                                          "pylsp")))))

Eglot itself does not require any further tuning for me, however there are some things to take into consideration with Python. Namely, depending on the lsp server you choose you will have to set up your config per project differently. For example, with pyright you can have it installed at the system level and then include pyrightconfig.json files in the root of your project. However, this means you'll also have to keep these updated, and out of VC or builds by adding them to your gitignore and dockerignore files. Otherwise, using pylsp you can install the server in your virtual environment, this however brings in a dev dependency that not everybody in your team may need or want to use.

The following is the bare minimum I think is needed to work with Python using your favorite lsp and including support for tools I find useful or nowadays universally present in projects:

  • pyenv support to create or activate/deactivate python environments
  • black to format your code and order your imports
  • pyconf is a small tool I made to have a tool similar to pycharm's run config feature.
(use-package pyenv-mode
  :ensure t
  :init
  (add-to-list 'exec-path "~/.pyenv/shims")
  (setenv "WORKON_HOME" "~/.pyenv/versions/")
  :config
  (pyenv-mode))

(use-package pyconf
  :ensure t)

(defalias 'workon 'pyvenv-workon)

(use-package python-black
  :ensure t
  :demand t
  :after python
  :hook ((python-mode . python-black-on-save-mode)))

So far, for Go I haven't had to take into consideration any project configs or headaches, the golsp server works great out of the box with eglot and the only configs I have added are a couple of functions to compile and run files as I want. Also, I believe I copied these functions from somewhere on the interweb who knows how long ago.

(defun go-run-this-file ()
  "go run"
  (interactive)
  (compile (format "go run %s" (buffer-file-name))))

(defun go-compile ()
  "go compile"
  (interactive)
  (compile "go build -v && go test -v && go vet"))

(defun go-compile-debug ()
  "go compile with necessary flags to debug with gdb"
  (interactive)
  (compile "go build -gcflags=all=\"-N -l\""))

(use-package go-mode
  :ensure t
  :bind (("C-c C-k" . go-run-this-file)
         ("C-c C-c" . go-compile)
         ("C-c C-d" . go-compile-debug))
  :hook ((before-save . eglot-format-buffer)))

Autocomplete

One of the main features of these LSP servers is that they provide autocompletion. Nowadays the cool kids all use corfu, I like corfu quite a lot as well, but for larger python projects I have found that it does tend to hang for very long times when used with eglot. So I still use company mode. Feel free to use any of the two, actually, swapping them is so easy that I use a flag variable in my emacs config to decide which one of them to use.

(unless use-company
  (use-package corfu
    :after orderless
    ;; Optional customizations
    :custom
    (corfu-cycle t)                ;; Enable cycling for `corfu-next/previous'
    (corfu-auto t)                 ;; Enable auto completion
    (corfu-separator ?\s)          ;; Orderless field separator
    (corfu-quit-at-boundary nil)   ;; Never quit at completion boundary
    (corfu-quit-no-match nil)      ;; Never quit, even if there is no match
    (corfu-preview-current nil)    ;; Disable current candidate preview
    ;; (corfu-preselect-first nil)    ;; Disable candidate preselection
    ;; (corfu-on-exact-match nil)     ;; Configure handling of exact matches
    ;; (corfu-echo-documentation nil) ;; Disable documentation in the echo area
    (corfu-scroll-margin 5)        ;; Use scroll margin
    ;; Enable Corfu only for certain modes.
    :hook ((prog-mode . corfu-mode)
           (shell-mode . corfu-mode)
           (eshell-mode . corfu-mode))
    ;; Recommended: Enable Corfu globally.
    ;; This is recommended since Dabbrev can be used globally (M-/).
    ;; See also `corfu-excluded-modes'.
    :init
    (global-corfu-mode) ; This does not play well in eshell if you run a repl
    (setq corfu-auto t))
  (define-key corfu-map (kbd "M-p") #'corfu-popupinfo-scroll-down) ;; corfu-next
  (define-key corfu-map (kbd "M-n") #'corfu-popupinfo-scroll-up)  ;; corfu-previous
(when use-company
  (use-package company
    :ensure t
    :hook ((prog-mode . company-mode))
    :bind (:map company-active-map
                ("<return>" . nil)
                ("RET" . nil)
                ("C-<return>" . company-complete-selection)
                ([tab] . company-complete-selection)
                ("TAB" . company-complete-selection)))
  (use-package company-box
    :ensure t
    :hook (company-mode . company-box-mode)))

Project Navigation

Project.el

To navigate a project I use the built in project.el plus a small mode called project-tab-groups. It is quite useful if you use the built-in tab-bar-mode. This package will create a tab for every project that you open with project.el, and it will integrate quite nicely with your workflow if you use tab-bar-mode already.

(use-package project-tab-groups
  :ensure
  :config
  (project-tab-groups-mode 1))

(global-set-key (kbd "C-<next>") 'tab-next)
(global-set-key (kbd "C-<prior>") 'tab-previous)

With the built in project.el you can do several things like:

  • find a file in a project
  • find a matching regex
  • compile the project
  • start a shell or dired instance in the project
  • switch tab in a project (built in in Emacs 30)

Important buffers

After reading a nice article from the blog of Mastering Emacs on Emacs's window manager ( https://www.masteringemacs.org/article/demystifying-emacs-window-manager ) I have adopted the suggestion on setting up IDE style side windows and it changed my workflow for the better, since the compilations or embark export buffers always pop up in the same place, and they are not affected when I decide to delete all the other windows I have aside from the one I am currently in.

(setq window-sides-slots '(1 0 1 0))

(add-to-list 'display-buffer-alist
        `(,(rx (| "*compilation*" "*grep*" "*Embark Export" "*Occur"))
          display-buffer-in-side-window
          (side . right)
          (slot . 0)
          (window-parameters . ((no-delete-other-windows . t)))
          (window-width . 80)))

Nice to have's

Consult

Consult provides search commands and integrates well with project.el, running for example consult-ripgrep will find matches of the given pattern in your project. Consult works very nicely with other tools, like vertico, orderless, marginalia and Embark through embark-consult (check these out if you want to improve search functionality and the minibuffer). One of the selling points for me is the option of getting preview of the files that match your command search.

(use-package consult
    :ensure t
    :demand t
    :bind (("C-s" . consult-line)
           ("C-M-l" . consult-imenu)
           ("C-x b" . consult-buffer)
           ("C-x C-b" . consult-bookmark)
           ("C-M-s" . consult-ripgrep)
           :map minibuffer-local-map
           ("C-r" . consult-history)))

FS navigation

Dired is pretty cool, but dirvish makes it even cooler, with file previews, icons, directory unnesting and a toggable side buffer to quickly access your file system. My set up is mostly copied from the example one you can find in its repository https://github.com/alexluigit/dirvish

(use-package dirvish
    :ensure t
    :custom
    (dirvish-bookmarks-alist
     '(("h" "~/"                          "Home")
       ("d" "~/Downloads/"                "Downloads")
       ("m" "/mnt/"                       "Drives")))
    :config
    (dirvish-override-dired-mode)
    (dirvish-peek-mode)
    (setq dirvish-attributes '(all-the-icons file-size))
    :bind
    (:map dired-mode-map
          ("SPC" . dirvish-show-history)
          ("r"   . dirvish-roam)
          ("b"   . dirvish-goto-bookmark)
          ("f"   . dirvish-file-info-menu)
          ("M-a" . dirvish-mark-actions-menu)
          ("M-s" . dirvish-setup-menu)
          ("M-f" . dirvish-toggle-fullscreen)
          ([remap dired-summary] . dirvish-dispatch)
          ([remap dired-do-copy] . dirvish-yank)
          ([remap mode-line-other-buffer] . dirvish-other-buffer)))

In conclusion, setting up Eglot with your Python or Go LSP's in Emacs can provide a lightweight and efficient development environment. By using the snippets and references presented, you should be able to get started quickly and easily, and can always expand to make the editor tailored to your needs.