Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- The following describes how I have set up my nvim config to reliably work with
- language support in a large .NET 4 Framework codebase.
- It uses
- - Lazy.nvim as a package manager to install plugins.
- - csharp-ls as a langauge server
- Besides the main lspconfig this also uses csharpls-extended-lsp.nvim for decomp-goto-defintion.
- Note that csharp_ls currently only works with SDK style project files.
- There is also currently a bug that prevents csharp_ls from working if multiple
- target-frameworks are defined in a project file.
- Further down is also a workaround for that.
- Anyway here's the lazy-config for lsp-config.
- If you don't understand any of the following, I suggest you look at
- https://github.com/nvim-lua/kickstart.nvim
- which has extensive explanations for all of this.
- Reading through the help pages of lspconfig doesn't hurt either.
- ```lua
- require('lazy').setup({
- -- part of the lazy.setup
- { -- LSP Configuration & Plugins
- 'neovim/nvim-lspconfig',
- dependencies = {
- -- Automatically install LSPs and related tools to stdpath for neovim
- 'williamboman/mason.nvim',
- 'williamboman/mason-lspconfig.nvim',
- 'WhoIsSethDaniel/mason-tool-installer.nvim',
- { 'j-hui/fidget.nvim', opts = {} },
- { 'folke/neodev.nvim', opts = {} },
- },
- config = function()
- vim.api.nvim_create_autocmd('LspAttach', {
- group = vim.api.nvim_create_augroup('kickstart-lsp-attach', { clear = true }),
- callback = function(event)
- local map = function(keys, func, desc)
- vim.keymap.set('n', keys, func, { buffer = event.buf, desc = 'LSP: ' .. desc })
- end
- local builtin = require 'telescope.builtin'
- local lsp_buf = vim.lsp.buf
- -- map these however you want
- -- these are how kickstart.nvim sets them up
- map('gd', builtin.lsp_definitions, '[G]oto [D]efinition')
- map('gr', builtin.lsp_references, '[G]oto [R]eferences')
- map('<leader>gi', builtin.lsp_implementations, '[G]oto [I]mplementation')
- map('<leader>D', builtin.lsp_type_definitions, 'Type [D]efinition')
- map('<leader>ds', builtin.lsp_document_symbols, '[D]ocument [S]ymbols')
- map('<leader>ws', builtin.lsp_dynamic_workspace_symbols, '[W]orkspace [S]ymbols')
- map('<leader>rn', lsp_buf.rename, '[R]e[n]ame')
- map('<leader>ca', lsp_buf.code_action, '[C]ode [A]ction')
- map('K', lsp_buf.hover, 'Hover Documentation')
- map('gD', lsp_buf.declaration, '[G]oto [D]eclaration')
- end,
- })
- local capabilities = vim.lsp.protocol.make_client_capabilities()
- capabilities = vim.tbl_deep_extend('force', capabilities, require('cmp_nvim_lsp').default_capabilities())
- -- the following allows me to reliably force csharp_ls to load a specific solution
- -- This is accomplished by speficiying a "SOLUTION" enviroment variable before starting nvim
- -- reading it as a path, and then passing the absolute path to csharp_ls as a startup argument
- -- Usage would something like this
- -- CMD:
- -- > set SOLUTION=./Soultions/Main.sln
- -- > nvim .
- -- PWSH:
- -- > $env:SOLUTION = "./Solutions/Main.sln"
- -- > nvim .
- --
- -- -- and then in nvim :LspStart in a csharp file to start the language server
- -- -- it'll then prompt for the root_dir (see below)
- -- -- and then it loads
- local cmd = { 'csharp-ls' }
- local forceSolution = vim.env.SOLUTION
- if forceSolution then
- forceSolution = abs_norm_path(forceSolution)
- vim.notify('Solution: ' .. forceSolution)
- -- NOTE:that this will start the csharp-ls thats installed globally under your system, not the one that Mason installs
- -- You could replace this with the absolute path to csharp_ls that mason installs but this works too
- -- if you install it yourself with 'dotnet tool install --global csharp-ls'
- -- Note that you still need to have it installed with Mason so that lspconfig knows its availible
- cmd = { 'csharp-ls', '-s', forceSolution }
- end
- local lspconfig = require 'lspconfig'
- local servers = {
- csharp_ls = {
- -- autostart is disabled since I don't always want to load a huge solution when I just wanna look at a file
- -- Manually start with :LspStart
- autostart = false,
- cmd = cmd,
- --[[
- another important thing to know is the concept of the root directory in lspconfig and how it affects its behavior
- By default lspconfig tries to guess the root directory of a repo when opening a source file
- if there already is a language server running with that root_dir and a matching language
- then it attaches the file to the existing language server instead of starting a new server for that file
- For csharp_ls the default config is to walk up the file tree until you find a solution (*.sln)
- and if it can't find a solution, then walk up the file until it find a project (*.csproj)
- and if it can't find that if falls back to the current working directory
- However sometimes solutions include projects and their source code that are not within the parent directory of the solution
- So lsp-config would guess a different root_dir and think the source file belongs to a different repo,
- which it has to start a second language server for
- To make sure that doesn't happen, my root_dir callback prompts the user for the root dir,
- and once set, always returns that one root_dir
- it presets the root_dir it finds by default
- This setup prevents loading up two different c# langauge servers within one nvim instance
- which to me isn't an issue. If I need to open another separate repo, I can start a second nvim instance altogether
- Note that the Solution file can't be prompted dynamically in this way,
- since it has to be hard-coded into the cmd that starts the language sever during the time that lspconfig is configured
- which is when nvim starts up
- --]]
- root_dir = function(startpath)
- if vim.g.root_dir ~= nil then
- return vim.g.root_dir
- end
- local util = lspconfig.util
- local resultPath = util.root_pattern("*.sln")(startpath) or util.root_pattern('*.csproj')(startpath) -- default csharp_ls root_dir func
- if resultPath and resultPath == vim.g.root_dir then
- return resultPath
- end
- if resultPath == nil then
- resultPath = vim.fn.getcwd()
- end
- resultPath = vim.fn.input { prompt = "Project Root > ", default = resultPath, cancelreturn = resultPath }
- vim.g.root_dir = resultPath
- return resultPath;
- end,
- },
- }
- -- the rest if from the kickstart.nvim lsp setup
- require('mason').setup()
- -- You can add other tools here that you want Mason to install
- -- for you, so that they are available from within Neovim.
- local ensure_installed = vim.tbl_keys(servers or {})
- vim.list_extend(ensure_installed, {
- 'stylua', -- Used to format lua code
- })
- require('mason-tool-installer').setup { ensure_installed = {} }
- require('mason-lspconfig').setup {
- handlers = {
- function(server_name)
- local server = servers[server_name] or {}
- -- This handles overriding only values explicitly passed
- -- by the server configuration above. Useful when disabling
- -- certain features of an LSP (for example, turning off formatting for tsserver)
- server.capabilities = vim.tbl_deep_extend('force', {}, capabilities, server.capabilities or {})
- require('lspconfig')[server_name].setup(server)
- end,
- },
- }
- end,
- },
- -- this enabled decomiple goto-definition on nuget packages and corelib types
- -- note that this won't work under .NET framework for stdlib types.
- -- It just finds reference assemblies for those that don't contain the actual code
- -- it should still work for nuget packages and manuall assembly references under .NET framework
- -- as long as the assemlby refernces in the project file have their paths specified
- -- note that you will need to set a separate keybind for the decomp-goto-defintion command
- 'Decodetalkers/csharpls-extended-lsp.nvim',
- -- ..........rest of plugins here
- ```
- ### Manually attaching buffers to an existing language server
- If you already started your langauge server, and the file you have currently open
- wasn't attached to the server, (for example if it isn't under the root_dir),
- but you know it belongs to the same project, then you can force it to attach with the following command
- :lua vim.lsp.buf_attach_clien(0, 1)
- The first arg is the id of the buffer, with 0 always being the current buffer
- The second arg is the id of the language server, if you have started csharp_ls before any other language server
- then it should 1. But you can manually check which id it has with :LspInfo
- ### Working with multiple Target-Frameworks and csharp-ls
- I ran into that issue just recently as I have started migrating the code base I
- work on to .NET 8.
- My workaround for that was to dynamically set a single target framework accross projects
- ```csproj
- <Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <TargetFramework>$(dynamic_tf)</TargetFramework>
- // rest of csproj
- ```
- which is controlled via an external enviroment variable and a central `Directory.build.props`
- at the root of the repo. Which is imported into all projects automatically thanks to its naming scheme
- And it contains this:
- ```props
- <Project>
- <PropertyGroup>
- <!-- this defines a custom Property within the msbuild system
- which can be evaluated for any other valur with the $() syntax
- -->
- <dynamic_tf>net48</dynamic_tf>
- </PropertyGroup>
- <!-- msbuild projects are evaluted from top to bottom so this PropertyGroup overrides the value of dynamic_tf
- however it is only evaluated if the condition passes
- enviroment variables are also automatically set as properties so setting a TARGET_DOTNET_CORE enviroment variable with the value TRUE
- enables this PropertyGroup and therefore overrides the dynamic_tf variable
- -->
- <PropertyGroup Condition=" '$(TARGET_DOTNET_CORE)' == 'TRUE' ">
- <dynamic_tf>net8.0-windows</dynamic_tf>
- </PropertyGroup>
- // rest Directory.Build.props if any
- </Project>
- `
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement