Generics-aware gopls in Emacs

Posted on 3rd of March 2023 | 473 words

I needed to write some funky Go code using relatively new Go generics in it, just to quickly notice that my LSP client in Emacs didn’t recognise those like you would expect. Fixing gopls was relatively straight-forward, since my issue seemed to be just an old version. So I just installed gopls while having Go 1.18+ installed. In my case, I had already generics-aware Go version installed but seemed like that I had installed my gopls version before the generics update, so I had to just reinstall it with:

$ go install golang.org/x/tools/gopls@latest

After that gopls was pleased, or at least my eglot didn’t complain about syntactical errors in my code. Pretty much immediately I realised that now my goimports was broken, so it didn’t organise my imports accordingly. I knew that gopls was able to do this stuff instead of using goimports, but I just never was eager to fix something that was already working. But since now it was broken, I decided to find a way to fix it.

What I did was just uninstall goimports from my machine and started relying on gopls for organising my imports. Previously, I had set my Emacs so that when I saved my files, it always ran goimports (when working on Go files, of course). Setting my eglot to do that was relatively simple, but then I noticed that it only formats my code, it doesn’t automatically import the libraries like how goimports does.

Like I mentioned earlier, gopls should be able to work exactly like goimports in this case, so I had to start digging on how I can make my eglot to do this. Basically, how gopls does this, is it uses source.organizeImports action for it. So I needed to run that somehow on save.

Fortunately, eglot exports all these code actions that the LSP can do with a neat function called eglot-code-actions. After some tinkering, I was able to call that before the save:

(use-package go-mode
  :ensure t
  :preface
  (defun tok/gofmt-before-save ()
    (interactive)
    (gofmt-before-save)
    ;; Run `eglot-code-actions' only in buffers where `eglot' is active.
    (when (functionp 'eglot-code-actions)
      (eglot-code-actions nil nil "source.organizeImports" t)))
  :hook (go-mode . (lambda ()
                     ;; Using depth -10 will put this before eglot's
                     ;; willSave notification so that the notification
                     ;; reports the actual contents that will be
                     ;; saved.
                     (add-hook 'before-save-hook 'tok/gofmt-before-save -10 t))))

I decided to use gofmt-before-save, which comes from go-mode here since I noticed that if you would just run eglot-format-buffer formatting doesn’t open a new buffer where it lists all the errors and instead prints them in the LSPs messages. You can probably dig them somehow from there and print in a new buffer, but I liked already the existing behaviour of running gofmt with go-mode so I decided to use that one.

Small fix, but a really good one. Happy generics-aware hacking.