-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(lsp): support function for client root_dir #31630
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really understand the use-case here. The function would be evaluated within the lsp.start
call, which means whoever calls lsp.start
could call the root-dir function themselves and they'd have the same kind of evaluation ordering.
What am I missing?
I'd understand this more if it turned root_markers
from lsp.config
into a function, because there the evaluation would be deferred from definition to on-use.
Does this resolve neovim/nvim-lspconfig#1827 ? |
Just to mention, we're exploring the idea on matrix of adding workspace folders instead of a root_dir function. |
Adding a workspace folder in an on_init function seems to work ok at first glance. Example (for a Rust project): on_init = function(client)
vim.system({"cargo", "metadata", "--no-deps", "--format-version", "1"}, {
cwd = client.root_dir,
}, function(out)
if out.code ~= 0 then
return
end
local workspace_root = vim.json.decode(out.stdout).workspace_root
if workspace_root then
client:_add_workspace_folder(workspace_root)
end
end)
end The caveat here is that the client will now have 2 workspace folders: one is the workspace root found by To make this more concrete, if I have the following project layout:
If I first open The definition of "workspace folders" per the spec seems to imply that each folder should be an individual project root, which is not the case in the example above. So having a "dynamic" root_dir function that can set the client's root directory to the workspace still seems like it might be worth doing. |
Mentioned this in chat - posting here for the record too: I think for dynamic use cases the options are:
local function start(root_dir)
if type(root_dir) == "function" then
root_dir = root_dir()
end
end Only to support doing a A example for 2) local cmd = {"cargo", "metadata", "--no-deps", "--format-version", "1"}
vim.system(cmd, {}, function(out)
if out.code ~= 0 then
return
end
local workspace_root = vim.json.decode(out.stdout).workspace_root
vim.lsp.start({
root_dir = workspace_root,
...
})
end) For 1) One question is if the root_dir function needs to take a callback, or if it runs within a coroutine to support yielding values to make the sync case easy and the async case possible. |
For the sync case the function can return the value directly. For the async case, the function must return nil and call the callback, at some point. |
Sounds good. Might be a bit odd for when |
You're right. Ehh, this is getting complicated. I guess we can use some other sentinel value for either case. Maybe returning |
1d6170d
to
0962b1e
Compare
Can we do neither and just force it to be sync? |
0962b1e
to
1ba15bd
Compare
Given that the cargo metadata cmd came up as a first use-case I think it should support async. But I'd tend to either:
vim.lsp.config("clangd", {
root_dir = function(on_root_dir)
local cmd = {"cargo", "metadata", "--no-deps", "--format-version", "1"}
vim.system(cmd, {}, function(out)
if out.code == 0 then
local workspace_root = vim.json.decode(out.stdout).workspace_root
on_root_dir(workspace_root)
else
on_root_dir(nil)
end
end,
}) For synchronous cases it is not so bad either, given that a
vim.lsp.config("clangd", {
root_dir = function()
local cmd = {"cargo", "metadata", "--no-deps", "--format-version", "1"}
local co = coroutine.running()
vim.system(cmd, {}, function(out)
local root = out.code == 0 and vim.json.decode(out.stdout).workspace_root or nil
coroutine.resume(co, root)
end)
return coroutine.yield()
end,
}) With a future vim.lsp.config("clangd", {
root_dir = function()
local cmd = {"cargo", "metadata", "--no-deps", "--format-version", "1"}
vim.system(cmd, {}, vim.async.resume())
local out = corotuine.yield()
return out.code == 0 and vim.json.decode(out.stdout).workspace_root or nil
end,
}) Where ---@param co? thread
function vim.async.resume(co)
co = co or coroutine.running()
return function(...)
if coroutine.status(co) == "suspended" then
coroutine.resume(co, ...)
else
local args = {...}
vim.schedule(function()
assert(
coroutine.status(co) == "suspended",
"Incorrect use of coresume. Callee must have yielded"
)
coroutine.resume(co, unpack(args))
end)
end
end
end This would require a coroutine.wrap within the logic triggering the root function. Upside is that synchronous functions could work with a regular |
I'm quite strongly against 2. IMO a function should never yield from a coroutine unless it knows where that coroutine was started and what for. The current running coroutine may have requirements about what should be yielded. Additionally some distros build with 5.2 features which makes vim.async will be designed entirely around wrapping other functions that have continuation parameters. I don't see any other option other than 1, which is simple and meets the requirements. To keep the sync case simple the root_dir function can return a sentinel value to indicate that the callback is expected to be called. This will allow us to add a timeout (or not). |
Note that we already do this: Although in that case it was a bit more forced because it was the only way to support async operations for reverse-request handlers without breaking BWC.
Can you elaborate on that? I don't see how this pattern would break down with other async libraries - as the first example shows it doesn't even require a a) it runs within a coroutine and That said, I don't have a strong opinion on this, I merely wanted to put the option on the table and if there are some issues with it, it would be good to learn about them because I use the patttern in nvim-dap quite a bit and so far I didn't notice any issues (and I think nvim-dap-ui even uses a different async approach on top - making me think that if the approaches were conflicting I should've noticed by now). |
If root_dir is a function it is evaluated when the client is created to determine the root directory. This enables dynamically determining the root directory based on e.g. project or directory structure (example: finding a parent Cargo.toml file that contains "[workspace]" in a Rust project).
1ba15bd
to
1c30937
Compare
All the different async libs that exist at the moment have certain requirements on what can be yielded.
Both these async libs require uv functions are executed on the main thread, so the callback can continue within the coroutine. |
If root_dir is a function it is evaluated when the client is created to determine the root directory. This enables dynamically determining the root directory based on e.g. project or directory structure (example: finding a parent Cargo.toml file that contains "[workspace]" in a Rust project).
If root_dir is a function it is evaluated when the client is created to determine the root directory.
This enables dynamically determining the root directory based on e.g. project or directory structure (example: finding a parent Cargo.toml file that contains "[workspace]" in a Rust project).