Skip to content

js/ts language servers (eslint/biome/svelte/ts_ls/vtsls/tsgo) attach to files that are only in workspace directory (ex. directory contains lock file) #4015

@krukroman

Description

@krukroman

Description

Commit 8ad2d8d introduced changes that restrict mentioned language servers from attaching to files outside the workspace directory. While this behavior makes sense for some servers, it creates inconveniences for JavaScript/TypeScript development.

Problem:

  • General JS/TS files: Developers often edit standalone JS/TS files not bound to any workspace or project. The current restriction prevents language servers from attaching to these files, reducing functionality (e.g., linting, autocompletion, and diagnostics).
  • ESLint/Biome: These servers do not support single files and should only attach when configuration files (e.g., .eslintrc, biome.json) are present in the directory, even if the directory is not a formal workspace.
  • Svelte: Unlike JS/TS, the Svelte language server should likely only attach to files within a Svelte project, as standalone Svelte files are rare and less practical.

Expected Behavior:

  • JS/TS Language Servers: Should attach to any JS/TS file, regardless of workspace context, to support standalone file editing.
  • ESLint/Biome: Should attach to files when configuration files are present in the directory, even if it’s not a workspace.
  • Svelte Language Server: Should only attach to files within a Svelte project.

Current Behavior:

  • All mentioned servers are restricted from attaching to files outside the workspace directory,

Possible solutions

example of current eslint config
  root_dir = function(bufnr, on_dir)
    -- The project root is where the LSP can be started from
    -- As stated in the documentation above, this LSP supports monorepos and simple projects.
    -- We select then from the project root, which is identified by the presence of a package
    -- manager lock file.
    local project_root_markers = { 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', 'bun.lock' }
    local project_root = vim.fs.root(bufnr, project_root_markers)
    if not project_root then
      return nil
    end

    -- We know that the buffer is using ESLint if it has a config file
    -- in its directory tree.
    --
    -- Eslint used to support package.json files as config files, but it doesn't anymore.
    -- We keep this for backward compatibility.
    local filename = vim.api.nvim_buf_get_name(bufnr)
    local eslint_config_files_with_package_json =
      util.insert_package_json(eslint_config_files, 'eslintConfig', filename)
    local is_buffer_using_eslint = vim.fs.find(eslint_config_files_with_package_json, {
      path = filename,
      type = 'file',
      limit = 1,
      upward = true,
      stop = vim.fs.dirname(project_root),
    })[1]
    if not is_buffer_using_eslint then
      return nil
    end

    on_dir(project_root)
  end,

For eslint/biome servers, the root_dir function should:

  • Return if neither a root project marker nor a config file is found.
  • Call on_dir(config_file_directory) if only a config file is found (and no root project marker exists).

For ts_ls/tsgo/vtsls, the root_dir function should call on_dir(nil) if there is no project_root to allow these language servers to attach to standalone files, as suggested here by @justinmk.

Previous description

Summary

ts_ls server don't attach to javascript or typescript files when there are no root marker files in directory (ex. package-lock.json)

Steps to reproduce

  1. create minimal neovim config file.
Minimal init.lua
vim.o.clipboard = 'unnamedplus'
vim.o.softtabstop = 2
vim.o.shiftwidth = 2
vim.o.expandtab = true


-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
  local lazyrepo = "https://github.com/folke/lazy.nvim.git"
  local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
  if vim.v.shell_error ~= 0 then
    vim.api.nvim_echo({
      { "Failed to clone lazy.nvim:\n", "ErrorMsg" },
      { out, "WarningMsg" },
      { "\nPress any key to exit..." },
    }, true, {})
    vim.fn.getchar()
    os.exit(1)
  end
end
vim.opt.rtp:prepend(lazypath)

-- Make sure to setup `mapleader` and `maplocalleader` before
-- loading lazy.nvim so that mappings are correct.
-- This is also a good place to setup other settings (vim.opt)
vim.g.mapleader = " "
vim.g.maplocalleader = " "

-- Setup lazy.nvim
require("lazy").setup({
  spec = {
    "neovim/nvim-lspconfig",
    config = function()
      vim.lsp.enable("ts_ls")
    end,
  },
  -- Configure any other settings here. See the documentation for more details.
  -- colorscheme that will be used when installing plugins.
  install = { colorscheme = { "habamax" } },
  -- automatically check for plugin updates
  checker = { enabled = true },
})
  1. install localy ts_ls server npm install -g typescript typescript-language-server.
  2. run neovim with minimal config nvim -u /path/to/minimal_init.lua.
  3. start editing index.js file :e index.js<CR>.
  4. open lsp info :LspInfo<CR>. ts_ls is not an active lsp.
  5. start editing index.ts file :e index.js<CR>
  6. open lsp info :LspInfo<CR>. ts_ls is not an active lsp.
  7. run npm init -y.
  8. repeat steps from 4 to 7.
  9. create package-lock.json file.
  10. repeat steps from 4 to 7. now ts_ls attached to both index.js and index.ts files

Expected behavior

ts_ls server should always be attached to any javascript or typescript files regardless where these files are located

Current behavior

ts_ls server don't attach to javascript or typescript files when there are no root marker files in directory (ex. package-lock.json)

LspInfo

index.js LspInfo
- LSP log level : WARN
- Log path: /home/user/.local/state/nvim/lsp.log
- Log size: 0 KB

vim.lsp: Active Clients ~
- No active clients

vim.lsp: Enabled Configurations ~
- ts_ls:
  - cmd: { "typescript-language-server", "--stdio" }
  - commands: {
      ["editor.action.showReferences"] = <function 1>
    }
  - filetypes: javascript, javascriptreact, javascript.jsx, typescript, typescriptreact, typescript.tsx
  - handlers: {
      ["_typescript.rename"] = <function 1>
    }
  - init_options: {
      hostInfo = "neovim"
    }
  - on_attach: <function @/home/user/.local/share/nvim/lazy/nvim-lspconfig/lsp/ts_ls.lua:110>
  - root_dir: <function @/home/user/.local/share/nvim/lazy/nvim-lspconfig/lsp/ts_ls.lua:56>


vim.lsp: File Watcher ~
- file watching "(workspace/didChangeWatchedFiles)" disabled on all clients

vim.lsp: Position Encodings ~
- No active clients
index.ts LspInfo
- LSP log level : WARN
- Log path: /home/user/.local/state/nvim/lsp.log
- Log size: 0 KB

vim.lsp: Active Clients ~
- No active clients

vim.lsp: Enabled Configurations ~
- ts_ls:
  - cmd: { "typescript-language-server", "--stdio" }
  - commands: {
      ["editor.action.showReferences"] = <function 1>
    }
  - filetypes: javascript, javascriptreact, javascript.jsx, typescript, typescriptreact, typescript.tsx
  - handlers: {
      ["_typescript.rename"] = <function 1>
    }
  - init_options: {
      hostInfo = "neovim"
    }
  - on_attach: <function @/home/user/.local/share/nvim/lazy/nvim-lspconfig/lsp/ts_ls.lua:110>
  - root_dir: <function @/home/user/.local/share/nvim/lazy/nvim-lspconfig/lsp/ts_ls.lua:56>


vim.lsp: File Watcher ~
- file watching "(workspace/didChangeWatchedFiles)" disabled on all clients

vim.lsp: Position Encodings ~
- No active clients
index.js LspInfo with package-lock.json file in directory
- LSP log level : WARN
- Log path: /home/user/.local/state/nvim/lsp.log
- Log size: 0 KB

vim.lsp: Active Clients ~
- ts_ls (id: 1)
  - Version: ? (no serverInfo.version response)
  - Root directory: /tmp/nvim
  - Command: { "typescript-language-server", "--stdio" }
  - Settings: {}
  - Attached buffers: 1

vim.lsp: Enabled Configurations ~
- ts_ls:
  - cmd: { "typescript-language-server", "--stdio" }
  - commands: {
      ["editor.action.showReferences"] = <function 1>
    }
  - filetypes: javascript, javascriptreact, javascript.jsx, typescript, typescriptreact, typescript.tsx
  - handlers: {
      ["_typescript.rename"] = <function 1>
    }
  - init_options: {
      hostInfo = "neovim"
    }
  - on_attach: <function @/home/user/.local/share/nvim/lazy/nvim-lspconfig/lsp/ts_ls.lua:110>
  - root_dir: <function @/home/user/.local/share/nvim/lazy/nvim-lspconfig/lsp/ts_ls.lua:56>


vim.lsp: File Watcher ~
- file watching "(workspace/didChangeWatchedFiles)" disabled on all clients

vim.lsp: Position Encodings ~
- No buffers contain mixed position encodings
index.ts LspInfo with package-lock.json file in directory
- LSP log level : WARN
- Log path: /home/user/.local/state/nvim/lsp.log
- Log size: 0 KB

vim.lsp: Active Clients ~
- ts_ls (id: 1)
  - Version: ? (no serverInfo.version response)
  - Root directory: /tmp/nvim
  - Command: { "typescript-language-server", "--stdio" }
  - Settings: {}
  - Attached buffers: 1, 4

vim.lsp: Enabled Configurations ~
- ts_ls:
  - cmd: { "typescript-language-server", "--stdio" }
  - commands: {
      ["editor.action.showReferences"] = <function 1>
    }
  - filetypes: javascript, javascriptreact, javascript.jsx, typescript, typescriptreact, typescript.tsx
  - handlers: {
      ["_typescript.rename"] = <function 1>
    }
  - init_options: {
      hostInfo = "neovim"
    }
  - on_attach: <function @/home/user/.local/share/nvim/lazy/nvim-lspconfig/lsp/ts_ls.lua:110>
  - root_dir: <function @/home/user/.local/share/nvim/lazy/nvim-lspconfig/lsp/ts_ls.lua:56>


vim.lsp: File Watcher ~
- file watching "(workspace/didChangeWatchedFiles)" disabled on all clients

vim.lsp: Position Encodings ~
- No buffers contain mixed position encodings

Investigation

By using git bisect I found that this Commit 8ad2d8d introduced changes that broke server ability to attach to any javascript or typescript files outside of root directory which contains root marker files (ex. package-lock.json)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions