williamboman / nvim-lsp-installer

Further development has moved to https://github.com/williamboman/mason.nvim!
https://github.com/williamboman/mason.nvim
Apache License 2.0
2k stars 123 forks source link

API for retrieving the install path of a specified server #464

Closed ispringle closed 2 years ago

ispringle commented 2 years ago

Is your feature request related to a problem? Please describe.

It would be great if the API could be extended to include the install path of the server. IE if I queried the API for pyright it would return $USER/.local/share/nvim/lsp-installers/pyright/node_modules/.bin/pyright.

Describe the solution you'd like

Seems that it'd be nice if this was part of the existing get_server object, something like

local _, server = lsp_installer.get_server(server_name)
if server:is_installed() then
  local path = server:path_to_binary()
end

Describe potential alternatives you've considered

At the moment I have all paths hardcoded, but this is a bit awkward as it means I need to edit my nvim RC when I want to add a server. In an ideal situation whenever I install a server I would then merely have to reopen neovim and my config would grab all installed servers, get the path to the binary provided my lsp-installer, and then provide that to Navigator.nvim.

Additional context

No response

williamboman commented 2 years ago

Hello! What's the use case for this? If you're just interested in setting up the servers, just like you would via lspconfig, I'd recommend following the recommended minimal setup in the README, but I feel like this is for doing something more than that?

ispringle commented 2 years ago

Yes, so there is a plugin called Navigator. Navigator is a sort of LSP config manager and it also does some other stuff to make LSP servers more useful and automate away much of the config. In. addition it does some UI overhauling and unifies various metadata endpoints (Codelens, LSP, Treesitter, language-specific plugins, etc.). The plugin also comes with some support for your nvim-lsp-installer, however it isn't able to directly call LSP servers installed by nvim-lsp-config as they're not in the PATH. To use these two together you have to provide the full path to the server binaries, which is unwieldy. If there was a way to ask nvim-lsp-installer where the binary is located, or even just a path to the root directory of the queried server, than the integration between these two plugins could become seamless for the end user.

mjlbach commented 2 years ago

Hello! What's the use case for this? If you're just interested in setting up the servers, just like you would via lspconfig, I'd recommend following the recommended minimal setup in the README, but I feel like this is for doing something more than that?

FWIW I (as maintainer of lspconfig) strongly would prefer if this API was exposed and users were encouraged to use it, rather than the current recommendation in the README which obfuscates the lspconfig API in favor of nvim-lsp-installer's. I'm not planning on maintaining the setup API in the lspconfig rewrite, and would recommend nvim-lsp-installer be used more as an installer library rather than taking control of the setup process itself.

williamboman commented 2 years ago

If there was a way to ask nvim-lsp-installer where the binary is located, or even just a path to the root directory of the queried server, than the integration between these two plugins could become seamless for the end user.

Ah, gotcha. In that case I'd recommend not assuming there will always be a canonical executable that can be referenced. Some servers are for example executed via their runtime program directly, for example:

Also, to the extent possible, nvim-lsp-installer does not concern itself with how the command to start a server looks like. This is mostly governed by lspconfig. Instead, nvim-lsp-installer tries to only amend the PATH when setting up a server to include the location where the server executable(s) are installed, allowing neovim's RPC client to locate them using the command provided by lspconfig.

To access whatever settings nvim-lsp-installer adds to server configurations I'd recommend going through the server:get_default_options() method (:h nvim-lsp-installer.Server). This will return a table containing the minimum needed for the server to be located by lspconfig/neovim, e.g.:

local lspinstaller_servers = require("nvim-lsp-installer.servers")
local lspconfig = require("lspconfig")

local servers = { "rust_analyzer", "clangd" }
for _, server_name in ipairs(servers) do
    local ok, server = lspinstaller_servers.get_server(server_name)
    if ok then
        lspconfig[server_name].setup(server:get_default_options())
    else
       -- Server does not exist in lspinstaller
       lspconfig[server_name].setup {}
    end
end
williamboman commented 2 years ago

FWIW I (as maintainer of lspconfig) strongly would prefer if this API was exposed and users were encouraged to use it, rather than the current recommendation in the README which obfuscates the lspconfig API in favor of nvim-lsp-installer's. I'm not planning on maintaining the setup API in the lspconfig rewrite, and would recommend nvim-lsp-installer be used more as an installer library rather than taking control of the setup process itself.

I'd say one of the goals of this plugin (at least for now) is to also act as a proxy for setting up servers. Just to name some of the principles I'm following:

I agree the lsp_installer.on_server_ready() API is a bit awkward at times, but I don't want to make any fundamental changes without having a sound reason to. Do you have any suggestion or ideas how these plugins - lsp-installer, lspconfig, and dedicated server plugins (rust-tools et al) - could be better composed together? I'd be happy to proactively make changes that would better fit the future architecture of the builtin LSP, I'm not sure what these would be because I don't feel like I have a particularly good understanding of where things are headed

mjlbach commented 2 years ago

I'd say one of the goals of this plugin (at least for now) is to also act as a proxy for setting up servers.

In my (strong) opinion, nvim-lsp-installer should only concern itself with installing, not with taking over the setup process. This is extremely contrary to my desire as the maintainer of the built-in language server client and lspconfig. I don't see why installing language servers needs a dedicated plugin (or any special consideration), as opposed to a generic installer.nvim which installs linters/formatters/etc. By drawing this line there is now a dap-installer, lsp-installer, formatter-installer, etc. I am happy to add the requisite hooks in lspconfig to support that, although I'm fairly confident this could be enabled without any intervention.

It also splits all of our help resources, if someone asks now "how do I override settings?" there is an lspconfig answer, and there is an nvim-lsp-installer answer. In fact, many users suggest immediately/directly using nvim-lsp-installer to new users, which becomes the biggest support burden to me as these users are the most likely to have questions and lose the ability to easily reuse the many resources I have written on the discourse, lspconfig wiki, and lspconfig.txt. This is extremely confusing to users, and frustrating to me who does not use your plugin yet still has to field questions on its use.

When setting up servers imperatively in your config via lspconfig directly, there's an assumption that the servers are always installed and available.

There isn't any downside to enabling setup for a server that isn't installed other than printing an error message letting you know that the server isn't installed/executable.

I think servers should automatically be set up for you, if/when installed. I don't think there should be a need to manually manage these imperatively in your neovim config. Only when you want to customize your settings is when you should need to manually extend it.

I think we just fundamentally disagree on that. Neovim as a general principal is opt-in, most settings require some manual intervention to enable. If a user really wants everything to be setup for them (vscode) they should use a framework because I doubt the line stops at installing language servers automatically.

This is also extremely hard in practice due to the duplication in servers. If you have pyright and pylsp do you really want both to launch on a buffer? ccls and clangd? Furthermore, you almost have to do some declarative configuration because otherwise you need keybindings. If you are already writing a ton of configuration (see the example in the readme):

local lsp_installer_servers = require('nvim-lsp-installer.servers')

local servers = {
    "rust_analyzer",
    "clangd",
    "pyright",
}

-- Loop through the servers listed above and set them up. If a server is
-- not already installed, install it.
for _, server_name in pairs(servers) do
    local server_available, server = lsp_installer_servers.get_server(server_name)
    if server_available then
        server:on_ready(function ()
            -- When this particular server is ready (i.e. when installation is finished or the server is already installed),
            -- this function will be invoked. Make sure not to also use the "catch-all" lsp_installer.on_server_ready()
            -- function to set up your servers, because by doing so you'd be setting up the same server twice.
            local opts = {}
            server:setup(opts)
        end)
        if not server:is_installed() then
            -- Queue the server to be installed.
            server:install()
        end
    end
end

How is this easier to grok than:

local servers = {
    "rust_analyzer",
    "clangd",
    "pyright",
}

require('installer.nvim').ensure_installed(servers)

for _, lsp in pairs(servers) do
  require('lspconfig')[lsp].setup {
    on_attach = on_attach,
    flags = {
      -- This will be the default in neovim 0.7+
      debounce_text_changes = 150,
    }
  }
end

If you really wanted to also have the imperative lspinstall x with some default settings (like sharing that in your on_attach) I would define that as a separate interface/plugin from the installer itself.

local servers = {
    "rust_analyzer",
    "clangd",
    "pyright",
}

require('installer.nvim').ensure_installed(servers)

for _, lsp in pairs(servers) do
  require('lspconfig')[lsp].setup {
    on_attach = on_attach,
    flags = {
      -- This will be the default in neovim 0.7+
      debounce_text_changes = 150,
    }
  }
end

-- This wraps the above and returns a config table to pass to setup when installer.nvim triggers an `installed` autocommand.
require('imperative-lsp.nvim').setup{
on_attach = on_attach
}

-- This is what that might do under the hood
require('imperative-lsp.nvim').post_install_hooks =  {
function(binary_name)
require('lspconfig')[binary_name].setup {
    on_attach = on_attach,
    flags = {
      -- This will be the default in neovim 0.7+
      debounce_text_changes = 150,
    }
}
end
}

Do you have any suggestion or ideas how these plugins - lsp-installer, lspconfig, and dedicated server plugins (rust-tools et al) - could be better composed together?

My suggestion, which I've mentioned before, is that nvim-lsp-installer could inject itself into all configs by leveraging the global hook in setup to rewrite cmd (or modifypath) in the configuration. If the main issue is allowing imperative declaration of setup than nvim-lsp-installer can call setup under the hood based on which servers are installed while first checking if that server has been previously setup. Furthermore, in the event that you did want to have LspInstall x work, you just need to trigger a filetype autocommand after

The future of lspconfig is going to be something like (the as of yet unfinished) projects.nvim. Things like rust-tools, nvim-jdtls, etc. will define project templates which specify how to launch the server, detect the project, maybe provide integrations for formatters, etc.

williamboman commented 2 years ago

I don't see why installing language servers needs a dedicated plugin (or any special consideration), as opposed to a generic installer.nvim which installs linters/formatters/etc. By drawing this line there is now a dap-installer, lsp-installer, formatter-installer, etc.

I agree. There's https://github.com/nazo6/installer.nvim which I think looks quite nice, it takes this approach. The reason this plugin only targets LSP is because it simply started off as an experimental alternative to the :LspInstall originally found in lspconfig and then later by kabouzeid. Also, limiting the scope to LSPs only helps from a maintenance perspective. So far I've been preoccupied with things like adding more servers (with corresponding tests in nvim-lspconfig-test), general support & maintenance, evolving the UI component, etc. Repurposing what's already in place for other tooling like formatters and linters should be pretty straight forward.

It also splits all of our help resources, if someone asks now "how do I override settings?" there is an lspconfig answer, and there is an nvim-lsp-installer answer. In fact, many users suggest immediately/directly using nvim-lsp-installer to new users, which becomes the biggest support burden to me as these users are the most likely to have questions and lose the ability to easily reuse the many resources I have written on the discourse, lspconfig wiki, and lspconfig.txt. This is extremely confusing to users, and frustrating to me who does not use your plugin yet still has to field questions on its use.

Ah, I wasn't aware there was a support burden here. I've only seen the occasional question/confusion around the setup, which is one of the reasons why I've let the current setup mechanism remain as-is. I was planning to revisit it once the "lspconfig rewrite" is finalizing because I do want to improve it. Where are the questions coming from 👀, maybe I can help?

There isn't any downside to enabling setup for a server that isn't installed other than printing an error message letting you know that the server isn't installed/executable.

I saw you just removed this message, but it is (was) one of the reasons. Also, I like to defer things until they're actually needed, i.e. not setting up servers you know won't be able to start. There's also an affordance aspect to it - where the editor will report that server x is configured when, per my definition at least, it's not.

I think we just fundamentally disagree on that. Neovim as a general principal is opt-in, most settings require some manual intervention to enable. If a user really wants everything to be setup for them (vscode) they should use a framework because I doubt the line stops at installing language servers automatically.

I think people are generally starting to consider the capabilities offered by language servers to be a core component in any modern editor. Especially when "native LSP" is one of the more central value propositions of the editor. I think people's expectations aren't fully met when large portions of LSP management is externalized. Although this will be border-line comparing apples and oranges; I think you could make the same argument for treesitter - why should neovim automatically install and hook these up for you (albeit not in core yet but in a plugin)?

How is this easier to grok than:

That example is indeed a pretty "bad" one. I remember having no imagination when writing it, I just wanted to demonstrate some of the other capabilities, but the use case I ended up demonstrating is a bad one. There's a simpler snippet for auto-installing servers in the Wiki, I've also just now removed that particular one from the docs. I've been wanting to add a .ensure_installed() capability, but it just hasn't happened yet.

My suggestion, which I've mentioned before, is that nvim-lsp-installer could inject itself into all configs by leveraging the global hook in setup to rewrite cmd (or modifypath) in the configuration.

I've been struggling to understand how this would actually work. The lspconfig setup calls usually happens inside a user's init script - how can I, in a plugin-manager agnostic manner, ensure that nvim-lsp-installer has had an opportunity to load and register hooks prior to that? Where I'd ideally want to arrive at is for nvim-lsp-installer to only require installation, with no need to call into any Lua methods unless overriding default plugin-wide settings.

If the main issue is allowing imperative declaration of setup than nvim-lsp-installer can call setup under the hood based on which servers are installed while first checking if that server has been previously setup.

I've actually looked into this before, but struggled to find a nice way to do so. My experience has been that much of the state in lspconfig is contained inside closures that aren't readily available. Any ideas?

Furthermore, in the event that you did want to have LspInstall x work, you just need to trigger a filetype autocommand after

Dispatching FileType after server installation has actually caused some interop issues with other plugins (#188). I'd prefer going through Lua APIs to do this instead of programmatically dispatching autocmds, so it currently calls the .try_add*() methods on the manager.

mjlbach commented 2 years ago

So far I've been preoccupied with things like adding more servers (with corresponding tests in nvim-lspconfig-test), general support & maintenance, evolving the UI component, etc. Repurposing what's already in place for other tooling like formatters and linters should be pretty straight forward.

Ah, I wasn't aware there was a support burden here. I've only seen the occasional question/confusion around the setup, which is one of the reasons why I've let the current setup mechanism remain as-is. I was planning to revisit it once the "lspconfig rewrite" is finalizing because I do want to improve it. Where are the questions coming from 👀, maybe I can help?

Here's one from today

There's lots of questions on reddit, probably a few times a week on the matrix room, here's an unanswered one on the discourse, and I've had people share "minimal" configs on 3rd party language server issues using nvim-lsp-installer.

The lspconfig rewrite is basically https://github.com/mjlbach/projects.nvim/blob/main/lua/projects/lspconfig_wrapper.lua, It's much simpler. It generates a template project for each language server. I'm not sure how your current paradigm would work, it would likely require an "ensure_installed" type function. It wouldn't make sense for :LspInstall pyright to start a generic project launching pyright, for example.

Also, I like to defer things until they're actually needed, i.e. not setting up servers you know won't be able to start. There's also an affordance aspect to it - where the editor will report that server x is configured when, per my definition at least, it's not.

There is no cost to setting up servers though, this seems to introduce a ton of added complexity for IMO very little gain (it not showing up in :LspInfo?)

I think people are generally starting to consider the capabilities offered by language servers to be a core component in any modern editor. Especially when "native LSP" is one of the more central value propositions of the editor. I think people's expectations aren't fully met when large portions of LSP management is externalized. Although this will be border-line comparing apples and oranges; I think you could make the same argument for treesitter - why should neovim automatically install and hook these up for you (albeit not in core yet but in a plugin)?

Both nvim-treesitter and nvim-lspconfig follow similar philosophies, where the core APIs are internalized and anything involving 3rd party repositories (parsers, language servers is externalized). The principal difference being that treesitter parsers are also managed by nvim-treesitter. I'm not sure where "automatically install and hook these up for you" comes from though, it's not the default, you have to enable "ensure_installed". Also a treesitter is not stateful as it only operates on a single buffer and is run by an in-process library, and there's nothing to configure. As you said, apples and oranges.

The main reason I think that's ok to internalize and language servers are not, is that parsers generally depend only on the availability of a c compiler toolchain, so the installation is completely homogenous. Language servers depend on many different system dependencies (rust-c/cargo for rust, clang for c, elixir/mx for elixir, node for JS, etc.) which are typically under the domain of the OS package manager.

I've been struggling to understand how this would actually work. The lspconfig setup calls usually happens inside a user's init script - how can I, in a plugin-manager agnostic manner, ensure that nvim-lsp-installer has had an opportunity to load and register hooks prior to that? Where I'd ideally want to arrive at is for nvim-lsp-installer to only require installation, with no need to call into any Lua methods unless overriding default plugin-wide settings.

This is a fictional example, somewhere in your plugin (invoked by setup {})

require('lspconfig').register_setup_hook( function(server_name, config)
  local cmd = require('nvim-lsp-installer').ensure_installed(server_name)
  config.cmd = cmd
return config
)

Then any server that is setup {} by the user automatically has its cmd substituted (lazily), and is ensured that it is installed.

Another option would just be generally overriding the local path of neovim to shim anything installed by lsp-installer at the front.

I've actually looked into this before, but struggled to find a nice way to do so. My experience has been that much of the state in lspconfig is contained inside closures that aren't readily available. Any ideas?

Dispatching FileType after server installation has actually caused some interop issues with other plugins (https://github.com/williamboman/nvim-lsp-installer/issues/188). I'd prefer going through Lua APIs to do this instead of programmatically dispatching autocmds, so it currently calls the .try_add*() methods on the manager.

These are technical problems I can solve quite easily, I'm extremely motivated to remove wrapping setup {} so I'll do whatever to make that happen.

williamboman commented 2 years ago

I'll start exploring something like the following this weekend

local lsp_installer = require("nvim-lsp-installer")
local lspconfig = require("lspconfig")

lsp_installer.setup {
    autostart_servers = true,
    -- ....
}

-- With `autostart_servers = true` you'd only need to explicitly call `.setup {}` for servers whose default settings you want to override

lspconfig.sumneko_lua.setup {
  settings = {
     Lua = { ... }
   }
}
williamboman commented 2 years ago

I'll close this as I believe OP's question was answered