Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions lsp/biome.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
--- `biome` supports monorepos by default. It will automatically find the `biome.json` corresponding to the package you are working on, as described in the [documentation](https://biomejs.dev/guides/big-projects/#monorepo). This works without the need of spawning multiple instances of `biome`, saving memory.

local util = require 'lspconfig.util'
local typescript = require 'lspconfig.typescript'

---@type vim.lsp.Config
return {
Expand Down Expand Up @@ -40,22 +41,15 @@ return {
},
workspace_required = true,
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 root_markers = { 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', 'bun.lock' }
-- Give the root markers equal priority by wrapping them in a table
root_markers = vim.fn.has('nvim-0.11.3') == 1 and { root_markers, { '.git' } }
or vim.list_extend(root_markers, { '.git' })
local project = typescript.detect_project(bufnr)

-- exclude deno
if vim.fs.root(bufnr, { 'deno.json', 'deno.jsonc', 'deno.lock' }) then
if project and (project.kind == 'deno') then
return
end

-- We fallback to the current working directory if no project root is found
local project_root = vim.fs.root(bufnr, root_markers) or vim.fn.getcwd()
local project_root = project and project.root_dir or vim.fn.getcwd()

-- We know that the buffer is using Biome if it has a config file
-- in its directory tree.
Expand Down
22 changes: 8 additions & 14 deletions lsp/denols.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
--- }
--- ```

local typescript = require 'lspconfig.typescript'
local lsp = vim.lsp

local function virtual_text_document_handler(uri, res, client)
Expand Down Expand Up @@ -76,22 +77,15 @@ return {
'typescript.tsx',
},
root_dir = function(bufnr, on_dir)
-- The project root is where the LSP can be started from
local root_markers = { 'deno.lock' }
-- Give the root markers equal priority by wrapping them in a table
root_markers = vim.fn.has('nvim-0.11.3') == 1 and { root_markers, { '.git' } }
or vim.list_extend(root_markers, { '.git' })
-- exclude non-deno projects (npm, yarn, pnpm, bun)
local non_deno_path = vim.fs.root(
bufnr,
{ 'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', 'bun.lock' }
)
local project_root = vim.fs.root(bufnr, root_markers)
if non_deno_path and (not project_root or #non_deno_path >= #project_root) then
local project = typescript.detect_project(bufnr)

if not project then
return
end
-- We fallback to the current working directory if no project root is found
on_dir(project_root or vim.fn.getcwd())

if project.kind == 'deno' then
on_dir(project.root_dir)
end
end,
settings = {
deno = {
Expand Down
14 changes: 4 additions & 10 deletions lsp/eslint.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
--- /!\ When using flat config files, you need to use them across all your packages in your monorepo, as it's a global setting for the server.

local util = require 'lspconfig.util'
local typescript = require 'lspconfig.typescript'
local lsp = vim.lsp

local eslint_config_files = {
Expand Down Expand Up @@ -87,22 +88,15 @@ return {
end, {})
end,
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 root_markers = { 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', 'bun.lock' }
-- Give the root markers equal priority by wrapping them in a table
root_markers = vim.fn.has('nvim-0.11.3') == 1 and { root_markers, { '.git' } }
or vim.list_extend(root_markers, { '.git' })
local project = typescript.detect_project(bufnr)

-- exclude deno
if vim.fs.root(bufnr, { 'deno.json', 'deno.jsonc', 'deno.lock' }) then
if project and (project.kind == 'deno') then
return
end

-- We fallback to the current working directory if no project root is found
local project_root = vim.fs.root(bufnr, root_markers) or vim.fn.getcwd()
local project_root = project and project.root_dir or vim.fn.getcwd()

-- We know that the buffer is using ESLint if it has a config file
-- in its directory tree.
Expand Down
24 changes: 11 additions & 13 deletions lsp/ts_ls.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
--- It is recommended to use the same version of TypeScript in all packages, and therefore have it available in your workspace root. The location of the TypeScript binary will be determined automatically, but only once.
---

local typescript = require 'lspconfig.typescript'

---@type vim.lsp.Config
return {
init_options = { hostInfo = 'neovim' },
Expand All @@ -54,22 +56,18 @@ return {
'typescript.tsx',
},
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 root_markers = { 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', 'bun.lock' }
-- Give the root markers equal priority by wrapping them in a table
root_markers = vim.fn.has('nvim-0.11.3') == 1 and { root_markers, { '.git' } }
or vim.list_extend(root_markers, { '.git' })
local project = typescript.detect_project(bufnr)

if not project then
return
end

-- exclude deno
local deno_path = vim.fs.root(bufnr, { 'deno.json', 'deno.jsonc', 'deno.lock' })
local project_root = vim.fs.root(bufnr, root_markers)
if deno_path and (not project_root or #deno_path >= #project_root) then
if project.kind == 'deno' then
return
end
-- We fallback to the current working directory if no project root is found
on_dir(project_root or vim.fn.getcwd())

on_dir(project.root_dir)
end,
handlers = {
-- handle rename request for certain code actions like extracting functions / types
Expand Down
22 changes: 9 additions & 13 deletions lsp/tsgo.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
--- It is recommended to use the same version of TypeScript in all packages, and therefore have it available in your workspace root. The location of the TypeScript binary will be determined automatically, but only once.
---

local typescript = require 'lspconfig.typescript'

---@type vim.lsp.Config
return {
cmd = { 'tsgo', '--lsp', '--stdio' },
Expand All @@ -26,23 +28,17 @@ return {
'typescript.tsx',
},
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 root_markers = { 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', 'bun.lock' }
-- Give the root markers equal priority by wrapping them in a table
root_markers = vim.fn.has('nvim-0.11.3') == 1 and { root_markers, { '.git' } }
or vim.list_extend(root_markers, { '.git' })
local project = typescript.detect_project(bufnr)

-- exclude deno
if vim.fs.root(bufnr, { 'deno.json', 'deno.jsonc', 'deno.lock' }) then
if not project then
return
end

-- We fallback to the current working directory if no project root is found
local project_root = vim.fs.root(bufnr, root_markers) or vim.fn.getcwd()
-- exclude deno
if project.kind == 'deno' then
return
end

on_dir(project_root)
on_dir(project.root_dir)
end,
}
22 changes: 9 additions & 13 deletions lsp/vtsls.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
---
--- It is recommended to use the same version of TypeScript in all packages, and therefore have it available in your workspace root. The location of the TypeScript binary will be determined automatically, but only once.

local typescript = require 'lspconfig.typescript'

---@type vim.lsp.Config
return {
cmd = { 'vtsls', '--stdio' },
Expand All @@ -80,23 +82,17 @@ return {
'typescript.tsx',
},
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 root_markers = { 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', 'bun.lock' }
-- Give the root markers equal priority by wrapping them in a table
root_markers = vim.fn.has('nvim-0.11.3') == 1 and { root_markers, { '.git' } }
or vim.list_extend(root_markers, { '.git' })
local project = typescript.detect_project(bufnr)

-- exclude deno
if vim.fs.root(bufnr, { 'deno.json', 'deno.jsonc', 'deno.lock' }) then
if not project then
return
end

-- We fallback to the current working directory if no project root is found
local project_root = vim.fs.root(bufnr, root_markers) or vim.fn.getcwd()
-- exclude deno
if project.kind == 'deno' then
return
end

on_dir(project_root)
on_dir(project.root_dir)
end,
}
123 changes: 123 additions & 0 deletions lua/lspconfig/typescript.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
--- @brief
--- Utility for detecting the root directory of a TypeScript project

local M = {}

--- @class lspconfig.typescript.Project
--- @field kind '"node"' | '"deno"' | '"bun"'
--- @field root_dir string

--- @class lspconfig.typescript.Package
--- @field kind '"non-deno"' | '"deno"'
--- @field path string

--- @class lspconfig.typescript.Workspace
--- @field kind '"node"' | '"deno"' | '"bun"'
--- @field root_dir string

--- Detect the TypeScript project in the current working directory.
--- @param source (integer | string)
--- @return lspconfig.typescript.Project?
function M.detect_project(source)
-- The search scope limit is the closer of either the current directory or the closest Git repository directly above the file.
-- This prevents false positives if files like package-lock.json exist in the home directory.
local cwd = vim.fn.getcwd()
local git_root = vim.fs.root(source, '.git')
local root = (git_root and #git_root >= #cwd) and git_root or cwd

-- First, we look for configuration files that indicate the presence of a specific runtime under the root.

--- @type lspconfig.typescript.Package[]
local packages = {}

local non_deno_package = vim.fs.root(source, { 'package.json' })
if non_deno_package and (#non_deno_package >= #root) then
table.insert(packages, { kind = 'non-deno', path = non_deno_package })
end

local deno_package = vim.fs.root(source, { 'deno.json', 'deno.jsonc' })
if deno_package and (#deno_package >= #root) then
table.insert(packages, { kind = 'deno', path = deno_package })
end

table.sort(packages, function(a, b)
return #a.path > #b.path
end)

--- @type lspconfig.typescript.Package?
local closest_package = (#packages == 1 or (#packages > 1 and #packages[1].path > #packages[2].path)) and packages[1]
or nil

if not closest_package then
-- If no package is found, there is no useful information regarding the project.
return nil
end

-- Second, we look for a lock file that can reliably distinguish between runtime kind.

--- @type lspconfig.typescript.Workspace[]
local workspaces = {}

local node_workspace_root = vim.fs.root(source, { 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml' })
if node_workspace_root and (#root <= #node_workspace_root) and (#node_workspace_root <= #closest_package.path) then
table.insert(workspaces, { kind = 'node', root_dir = node_workspace_root })
end

local deno_workspace_root = vim.fs.root(source, { 'deno.lock' })
if deno_workspace_root and (#root <= #deno_workspace_root) and (#deno_workspace_root <= #closest_package.path) then
table.insert(workspaces, { kind = 'deno', root_dir = deno_workspace_root })
end

local bun_workspace_root = vim.fs.root(source, { 'bun.lock', 'bun.lockb' })
if bun_workspace_root and (#root <= #bun_workspace_root) and (#bun_workspace_root <= #closest_package.path) then
table.insert(workspaces, { kind = 'bun', root_dir = bun_workspace_root })
end

-- If there is only one directory that is closest, this is the workspace we should find.
-- If there are multiple workspaces pointing to the same directory, or if no workspaces are found,
-- `detected_workspace` will be nil as there is no useful information regarding the workspace.

table.sort(workspaces, function(a, b)
return #a.root_dir > #b.root_dir
end)

--- @type lspconfig.typescript.Workspace?
local detected_workspace = (
#workspaces == 1 or (#workspaces > 1 and #workspaces[1].root_dir > #workspaces[2].root_dir)
)
and workspaces[1]
or nil

-- If no workspace is detected, the closest package directly above the file will be detected.
if not detected_workspace then
if closest_package.kind == 'deno' then
return { kind = 'deno', root_dir = closest_package.path }
else
return { kind = 'node', root_dir = closest_package.path }
end
end

-- If a non-Deno workspace is detected:
-- - If the immediate parent package is non-Deno, it is considered a package within that workspace, and root_dir is set to the workspace root.
-- - If the immediate parent package is Deno, it is considered a single Deno project located under an unrelated non-Deno workspace.
if detected_workspace.kind == 'node' or detected_workspace.kind == 'bun' then
if closest_package.kind == 'non-deno' then
return { kind = detected_workspace.kind, root_dir = detected_workspace.root_dir }
else
return { kind = 'deno', root_dir = closest_package.path }
end
end

-- If a Deno workspace is detected:
-- A package.json file within a Deno workspace cannot be used as a determining factor,
-- as it might be leveraging Deno's first-class package.json support.
-- Therefore, it is immediately considered a package within a Deno workspace.
-- See: https://docs.deno.com/runtime/fundamentals/node/#first-class-package.json-support
if detected_workspace.kind == 'deno' then
return { kind = 'deno', root_dir = detected_workspace.root_dir }
end

return nil
end

return M