How to correctly format a buffer with multiple LSP servers (incl. solution)

Problem

When you are in a buffer that has multiple lsp-servers running which support formatting, all of them will be triggered when calling +format/buffer or when configuring :editor (format +onsave) and saving a file.

As a result, the buffer won’t be formatted correctly. For example, missing whitespaces will be added multiple times and excess whitespace will be removed multiple times.

As an example, we can have a look at a TypeScript buffer with two running lsp-servers:

  1. ts-ls
  2. eslint

Before saving, the buffer looks like this:

class Foo {
    private bar = 'bar';
      private baz = 'baz';
}

The second property should be moved two spaces to the left. When saving, the “space” will be removed twice, leaving:

class Foo {
    private bar = 'bar';
    ivate baz = 'baz';
}

Instead, we want only eslint formatting, when eslint is available. If it isn’t, we want to fall back to the default.

Solution

I found the solution in a discussion on Emacs LSP’s discord.

In short, add this to your config.el:

;; Make sure that TypeScript files only get formatted once, with eslint when present.
(setq-hook! 'typescript-mode-hook +format-with-lsp nil)
(after! lsp-mode
  (defun my/eslint-format ()
    (interactive
     (if-let ((eslint (-first (lambda (wks)
                                (eq 'eslint (lsp--client-server-id
                                             (lsp--workspace-client wks))))
                              (lsp-workspaces))))
         (with-lsp-workspace eslint
           (lsp-format-buffer))
       (lsp-format-buffer))))
  (setq-hook! 'typescript-mode-hook +format-with 'my/eslint-format))
  1. Disable doom’s lsp formatting for TypeScript.
  2. Use the new function my/eslint-format, which uses only eslint, if available, and the default behavior otherwise.

Notes

I am sure this can be done in a better way, as I am quite noob to both Emacs and Doom. Maybe with better support for more modes. But hey, it works :grimacing: .

I wasn’t certain which category to put this in. Could also be a guide? But there it says “curated” so I shied away :sweat_smile: .

1 Like

Apart from :format module being long overdue for re-implementation, which would solve quite a lot of problems, I think your problem also could be solved in a more general way.

What you describe is basically a fallback hierarchy. Formatting with LSP is pretty well defined – a server either has the capability to textDocument/formatting or it does not. Going from there, one of the language servers can be prioritized to provide formatting. This is something the upcoming :format module could do and e.g. prefer add-on language servers (such as ESLint) to the main language server’s (ts-ls in this case) falling back to whatever format-all-the-code (or, later, apheleia) provides (in this case, either prettier or standard, whatever you have installed).

I think this can work pretty well, now to wait for :format rewrite :)

To quote a great neovim dev and

be the PR you want to see in the repo

do what you will with this information

1 Like

The PR might be refused, because it’s half in the Do not PR list. But nothing prevents you from making a private module in your config to try it out though.

If you do so, don’t hesitate to share it here in Discourse (#dev) or in Discord (#contributing) to get some feedback

1 Like

I think the PR in question would be the one implementing apheleia, which would probably be welcomed. However, I’m sure Henrik has a lot of ideas about that module which are not (yet) public, so that this PR might indeed be more hassle than it’s worth.

(Besides that: I havent’t authored a line of Elisp in my life and I’m currently content with :format as it is; format-all-the-code works as expected for me and even works when unpinned, which isn’t supposed to work at all)

1 Like