Using eslint language server as a formatter (fix all eslint errors)

The VSCode eslint extension provides an eslint language server. nvim-lsp-config has a configuration for it. I use this and it mostly works well, but I can’t get the server to function properly as a formatter. Specifically, I would like to have the formatter run the ESLintFixAll command (which works) when I invoke eslint as a formatter with vim.lsp.buf.formatting()

The settings from nvim-lsp-config are below. You can see that format is enabled. The vscode-eslint README are unclear and mixes discussion of VSCode configuration with the language server configuration, so I can’t figure out what/if I need to change below to make it work.

Does anyone have the eslint language server working to fix all errors on format?

settings = {
    validate = 'on',
    packageManager = 'npm',
    useESLintClass = false,
    codeActionOnSave = {
      enable = false,
      mode = 'all',
    },
    format = true,
    quiet = false,
    onIgnoredFiles = 'off',
    rulesCustomizations = {},
    run = 'onType',
    -- nodePath configures the directory in which the eslint server should start its node_modules resolution.
    -- This path is relative to the workspace folder (root dir) of the server instance.
    nodePath = '',
    -- use the workspace folder location or the file location (if no workspace folder is open) as the working directory
    workingDirectory = { mode = 'location' },
    codeAction = {
      disableRuleComment = {
        enable = true,
        location = 'separateLine',
      },
      showDocumentation = {
        enable = true,
      },
    },
  },

I’m not sure I see what you are asking for here. Formatting and fix-all are entirely separate requests to the language server.

Fix all is called with this request:

  request(0, 'workspace/executeCommand', {
    command = 'eslint.applyAllFixes',
    arguments = {
      {
        uri = vim.uri_from_bufnr(bufnr),
        version = lsp.util.buf_versions[bufnr],
      },
    },
  })

Formatting is a textDocument/formatting request

client.request('textDocument/formatting', params, nil, bufnr)

If you want to send both, one option would be in your on_attach for eslint/whatever servers you usually use eslint with, to map the formatting keybindg/autocommand to call a utility function that tries to call eslintfixall (the sync version, you can’t use async here) and then calls the formatting request.

Thanks for responding so promptly. Sorry my OP wasn’t clear-- I don’t expect formatting to literally run the EsLintFixAll command, but I expect it to do the same thing. What is the eslint formatter supposed to do if not fix all the eslint errors? And I checked out the source and that’s what it looks like. Here is the relevant snippet (scroll to bottom of flie) from vscode-eslint:

messageQueue.registerRequest(DocumentFormattingRequest.type, (params) => {
	const textDocument = documents.get(params.textDocument.uri);
	if (textDocument === undefined) {
		return [];
	}
	return computeAllFixes({ uri: textDocument.uri, version: textDocument.version }, AllFixesMode.format);
}, (params) => {
	const document = documents.get(params.textDocument.uri);
	return document !== undefined ? document.version : undefined;
});

On further investigation, it appears that eslint is not being registered as a formatter at all in neovim, despite the format = true in the settings. I’ve confirmed this by watching the LSP debug logs wihle running vim.lsp.buf.formatting_seq_sync(), I get nothing for a buffer in which the ESLint server is otherwise working fine.

Can you send me a minimal init.lua with eslint set up and a repo with file that will be changed upon running eslintfixall? I’m guessing for some reason the capability is not returned correctly.

I’ll do that if I can’t figure this out but I’m getting closer to a solution. This is some kind of path resolution issue, I notice now that this message is being logged on server startup:

      ['eslint/noLibrary'] = function()
        vim.notify('[lspconfig] Unable to find ESLint library.', vim.log.levels.WARN)
        return {}
      end,

I can also see in the source that formatting depends directly on this library being resolved, so that explains why formatting doesn’t work. The weird thing is that other server functionality does work.

OK, you were right about capabilities, the issue is that somehow <eslint_client>.resolved_capabilities.document_formatting ends up set to false when it should be true.. I was able to patch the problem with:

  on_init = function(client, initialize_result)
    client.resolved_capabilities.document_formatting = true
  end

Pretty sure this has something to do with vscode-eslint trying to register this capability dynamically, because the log has messages like this:

[WARN][2022-01-29 14:48:14] ...lsp/handlers.lua:109	"The language server eslint triggers a registerCapability handler despite dynamicRegistration set to false. Report upstream, this warning is harmless"

UPDATE: Looks like you were discussing this several months ago: add documentFormattingProvider to server capabilities response by williamboman · Pull Request #1307 · microsoft/vscode-eslint · GitHub

Consider my post an upvote for implementing dynamic registration in NeoVim, rightly or wrongly many language servers don’t seem to work properly without it…

Here’s my setup.

It works OK so far, with one issue, occasionally, when writing a file that eslint should fix-on-save, I get

ESLint: ENOENT: no such file or directory, realpath '/Users/bennyp/Developer/path/to/file.ts'. Please see the 'ESLint' output channel for det
ails.

Request Actions:
1. Open Output
Type number and <Enter> or click with the mouse (q or empty cancels):

hitting 1 just closes the notification and moves on as if nothing happened

This still happens despite the sleep 200m I added to the autocmd

Update, with the new lsp-installer guidelines (i.e. to just use the lsp_config api) I’m doing this:

--- Neovim's LSP client does not currently support dynamic capabilities registration, so we need to set
--- the resolved capabilities of the eslint server ourselves!
---
---@param  allow_formatting boolean whether to enable formating
---
local function toggle_formatting(allow_formatting)
  return function(client)
    default_on_attach(client)
    client.resolved_capabilities.document_formatting = allow_formatting
    client.resolved_capabilities.document_range_formatting = allow_formatting
  end
end

-- Install these, k?
-- Specify server options and settings per server by adding an options table
-- servers with `false` options table use the default on_attach function
--
local servers = {
  ['eslint'] = {
    on_attach = toggle_formatting(true),
    root_dir = lsp_util.find_git_ancestor,
    settings = {
      autoFixOnSave = true,
    },
  },
}

lsp_installer.setup {
  ensure_installed = vim.tbl_keys(servers)
}

-- Loop through the servers listed above.
-- installing each, then if install succeeded,
-- setup the server with the options specified in server_opts,
-- or just use the default options
--
for name, opts in pairs(servers) do
  lsp_config[name].setup(vim.tbl_extend('force', { on_attach = default_on_attach }, opts or {}))
end

local function can_autofix(client)
  return client.config.settings.autoFixOnSave or false
end

local function format_on_save()
  local clients = vim.lsp.get_active_clients()
  local can_autofix_clients = vim.tbl_filter(can_autofix, clients)
  if #can_autofix_clients > 0 then
    vim.lsp.buf.formatting_seq_sync(nil, 2000)
  end
end

vim.api.nvim_create_augroup('LspFormatting', { clear = true })
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '<buffer>',
  group = 'LspFormatting',
  callback = format_on_save
})

This works, but for reasons I haven’t been able to divine, only after I PackerCompile, and I still occasionally get the error from above

This is really way harder than it should be

3 Likes