Home

Awesome

nvim-eslint

nvim-eslint is a plugin designed to integrate ESLint functionality within Neovim. This plugin leverages the vscode-eslint language server and nvim native language client API to provide linting and code analysis capabilities directly in Neovim.

Why

I initially attempted to use the ESLint LSP provided by mason-lspconfig.nvim, which utilizes vscode-langservers-extracted as the ESLint language server and nvim-lspconfig for configuring the Neovim language server client. Unfortunately, this setup did not work well with my monorepo managed by pnpm workspaces. The various projects under the packages/ directory failed to function simultaneously.

While troubleshooting, I discovered that the available documentation was sparse, and debugging was nearly impossible. The vscode-eslint plugin documentation focuses on configuring its own client, which differs from the parameters received by the server. Additionally, the server configuration provided by nvim-lspconfig was outdated and challenging to update. Understanding the client code from nvim-lspconfig and the server code from vscode-eslint without live debugging proved to be extremely difficult. To enable live debugging, building the server from source was necessary, as opposed to using the pre-packaged version from vscode-langservers-extracted. The complexity of nvim-lspconfig, which supports multiple languages, made it more practical to develop my own solution.

Therefore, I decided to start from simple, and tried to understand all configurations of ESLint server.

Features

Installation

Prerequisite

node and ESLint needs to be installed

vim-plug

Plug 'esmuellert/nvim-eslint'

Quickstart

Add this to your lua configuration file:

require('nvim-eslint').setup({})

In many cases, your repository might function with the default configurations. However, similar to the vscode-eslint plugin, you may need to adjust some settings to ensure compatibility. This is especially true for complex repository structures like monorepos or when using the latest flat config files in ESLint 9. The following section explains how to customize these default settings.

Configuration

Default Configuration

{
    -- Toggle debug mode for ESLint language server, see debugging part
    debug = false,

    -- Command to launch language server. You might hardly want to change this setting
    cmd = M.create_cmd(),

    -- root_dir is used by Neovim LSP client API to determine if to attach or launch new LSP
    -- The default configuration uses the git root folder as the root_dir
    -- For monorepo it can have many projects, so launching too many LSP for one workspace is not efficient
    -- You can override it with passing function(bufnr)
    -- It should receive active buffer number and return root_dir
    root_dir = M.resolve_git_dir(args.buf),

    -- A table used to determine what filetypes trigger the start of LSP
    filetypes = { 'javascript', 'javascriptreact', 'javascript.jsx', 'typescript', 'typescriptreact',
    'typescript.tsx', 'vue', 'svelte', 'astro'},

    -- The client capabilities for LSP Protocol. See Nvim LSP docs for details
    -- It uses the default Nvim LSP client capabilities. Adding the capability to dynamically change configs
    capabilities = M.make_client_capabilities(),

    handlers = {
        -- The handlers handles language server responses. See Nvim LSP docs for details
        -- The default handlers only has a rewrite of default "workspace/configuration" handler of Nvim LSP
        -- Basically, when you load a new buffer, ESLint LSP requests the settings with this request
        -- To make it work with monorepo, the workingDirectory setting needs to be calculated at runtime
        -- This is the main reaason for rewriting, and it also works if you have a simple structure repo
        -- You might add more custom handler with reference to LSP protocol spec and vscode-eslint code
    },

    -- The settings send to ESLint LSP. See below part for details.
    settings = {
        validate = 'on',
        -- packageManager = 'pnpm',
        useESLintClass = true,
        useFlatConfig = function(bufnr)
            return M.use_flat_config(bufnr)
        end,
        experimental = { useFlatConfig = false },
        codeAction = {
            disableRuleComment = {
                enable = true,
                location = 'separateLine',
            },
            showDocumentation = {
                enable = true,
            },
        },
        codeActionOnSave = { mode = 'all' },
        format = false,
        quiet = false,
        onIgnoredFiles = 'off',
        options = {},
        rulesCustomizations = {},
        run = 'onType',
        problems = { shortenToSingleLine = false },
        nodePath = function(bufnr)
            return M.resolve_node_path()
        end,
        workingDirectory = { mode = 'location' },
        workspaceFolder = function(bufnr)
            local git_dir = M.resolve_git_dir(bufnr)
            return {
                uri = vim.uri_from_fname(git_dir),
                name = vim.fn.fnamemodify(git_dir, ':t'),
            }
        end,
    }
}

Settings Options

ESLint LSP settings are exactly the same as the parameters that vscode-eslint langage server receives:

https://github.com/microsoft/vscode-eslint/blob/790646388696511b2665a4d119bf0fb713dd990d/%24shared/settings.ts#L156-L178

These are the explanations for each option above. All function(bufnr) mentioned here is a Lua function that receives the number of the active buffer and returns the corresponding setting option. It is used to dynamically calculate some options, which is required for complicated scenarios.

All vscode-eslint settings options mentioned below is located at their docs.

Debugging

If the linter doesn't work for your project, there are several ways to troubleshoot.

First, enable debug level logs for Nvim LSP with vim.lsp.set_log_level('debug'). This will show detailed requests and responses sent to and received from the ESLint language server, helping you identify issues related to configuration, handlers, or the language server startup.

If the issue seems to originate from the ESLint language server itself, you can attach to the Node.js process for debugging:

  1. Run the build-eslint-language-server.sh script in the root folder of the repo with the debug option: ./build-eslint-language-server.sh --debug. This will clone the vscode-eslint project and compile the language server with source maps enabled, allowing you to set breakpoints in TypeScript files.
  2. Open VSCode from the root folder of the repository. Then, set debug = true in the plugin configuration and open a buffer you want to lint. The language server will start when you click start debugging in the VSCode debug tab. The launch.json file contains the correct debugger configuration, so it should work immediately.
    • This should also work with other debuggers as long as you can attach to the language server process and enable source maps. Feel free to share steps for other debuggers via a PR.
  3. Set breakpoints and debug. The key files to focus on are vscode-eslint/server/src/eslint.ts and vscode-eslint/server/src/eslintServer.ts. The eslint.ts file contains the language server logic, while eslintServer.ts contains handlers.
    • Common issues often relate to configurations, so set a breakpoint at this line to check if the language server receives the correct configuration.
    • Ensure the ESLint path is correctly resolved at this line, which is a common issue for monorepos.
    • You may also need to review the vscode-eslint language server client code to understand the correct client behavior if you plan to modify the Nvim LSP client. The client code is located at vscode-eslint/client/src/client.ts.

Contributing

Contributions are welcome! Please fork the repository and submit a pull request for any enhancements or bug fixes.

License

nvim-eslint is licensed under the MIT License. See the LICENSE file for more details.

Third Party Licenses

We used the original ESLint language server developed by Microsoft. Their license is: LICENSE

The ESLint LSP configs are partly taken from nvim-lspconfig. Their license is: LICENSE

As mentioned above we rewrite a Neovim LSP handler. Their license is: LICENSE