Advertisement
Guest User

csharp-ls setup for nvim

a guest
Nov 16th, 2024
71
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.40 KB | Software | 0 0
  1. The following describes how I have set up my nvim config to reliably work with
  2. language support in a large .NET 4 Framework codebase.
  3. It uses
  4. - Lazy.nvim as a package manager to install plugins.
  5. - csharp-ls as a langauge server
  6.  
  7. Besides the main lspconfig this also uses csharpls-extended-lsp.nvim for decomp-goto-defintion.
  8.  
  9. Note that csharp_ls currently only works with SDK style project files.
  10. There is also currently a bug that prevents csharp_ls from working if multiple
  11. target-frameworks are defined in a project file.
  12. Further down is also a workaround for that.
  13.  
  14. Anyway here's the lazy-config for lsp-config.
  15.  
  16. If you don't understand any of the following, I suggest you look at
  17. https://github.com/nvim-lua/kickstart.nvim
  18. which has extensive explanations for all of this.
  19. Reading through the help pages of lspconfig doesn't hurt either.
  20.  
  21. ```lua
  22.  
  23. require('lazy').setup({
  24. -- part of the lazy.setup
  25. { -- LSP Configuration & Plugins
  26. 'neovim/nvim-lspconfig',
  27. dependencies = {
  28. -- Automatically install LSPs and related tools to stdpath for neovim
  29. 'williamboman/mason.nvim',
  30. 'williamboman/mason-lspconfig.nvim',
  31. 'WhoIsSethDaniel/mason-tool-installer.nvim',
  32. { 'j-hui/fidget.nvim', opts = {} },
  33. { 'folke/neodev.nvim', opts = {} },
  34. },
  35. config = function()
  36. vim.api.nvim_create_autocmd('LspAttach', {
  37. group = vim.api.nvim_create_augroup('kickstart-lsp-attach', { clear = true }),
  38. callback = function(event)
  39. local map = function(keys, func, desc)
  40. vim.keymap.set('n', keys, func, { buffer = event.buf, desc = 'LSP: ' .. desc })
  41. end
  42.  
  43. local builtin = require 'telescope.builtin'
  44. local lsp_buf = vim.lsp.buf
  45.  
  46. -- map these however you want
  47. -- these are how kickstart.nvim sets them up
  48. map('gd', builtin.lsp_definitions, '[G]oto [D]efinition')
  49. map('gr', builtin.lsp_references, '[G]oto [R]eferences')
  50. map('<leader>gi', builtin.lsp_implementations, '[G]oto [I]mplementation')
  51. map('<leader>D', builtin.lsp_type_definitions, 'Type [D]efinition')
  52. map('<leader>ds', builtin.lsp_document_symbols, '[D]ocument [S]ymbols')
  53. map('<leader>ws', builtin.lsp_dynamic_workspace_symbols, '[W]orkspace [S]ymbols')
  54. map('<leader>rn', lsp_buf.rename, '[R]e[n]ame')
  55. map('<leader>ca', lsp_buf.code_action, '[C]ode [A]ction')
  56. map('K', lsp_buf.hover, 'Hover Documentation')
  57. map('gD', lsp_buf.declaration, '[G]oto [D]eclaration')
  58. end,
  59. })
  60.  
  61. local capabilities = vim.lsp.protocol.make_client_capabilities()
  62. capabilities = vim.tbl_deep_extend('force', capabilities, require('cmp_nvim_lsp').default_capabilities())
  63.  
  64. -- the following allows me to reliably force csharp_ls to load a specific solution
  65. -- This is accomplished by speficiying a "SOLUTION" enviroment variable before starting nvim
  66. -- reading it as a path, and then passing the absolute path to csharp_ls as a startup argument
  67. -- Usage would something like this
  68. -- CMD:
  69. -- > set SOLUTION=./Soultions/Main.sln
  70. -- > nvim .
  71. -- PWSH:
  72. -- > $env:SOLUTION = "./Solutions/Main.sln"
  73. -- > nvim .
  74. --
  75. -- -- and then in nvim :LspStart in a csharp file to start the language server
  76. -- -- it'll then prompt for the root_dir (see below)
  77. -- -- and then it loads
  78. local cmd = { 'csharp-ls' }
  79. local forceSolution = vim.env.SOLUTION
  80. if forceSolution then
  81. forceSolution = abs_norm_path(forceSolution)
  82. vim.notify('Solution: ' .. forceSolution)
  83. -- NOTE:that this will start the csharp-ls thats installed globally under your system, not the one that Mason installs
  84. -- You could replace this with the absolute path to csharp_ls that mason installs but this works too
  85. -- if you install it yourself with 'dotnet tool install --global csharp-ls'
  86. -- Note that you still need to have it installed with Mason so that lspconfig knows its availible
  87. cmd = { 'csharp-ls', '-s', forceSolution }
  88. end
  89.  
  90. local lspconfig = require 'lspconfig'
  91. local servers = {
  92. csharp_ls = {
  93. -- autostart is disabled since I don't always want to load a huge solution when I just wanna look at a file
  94. -- Manually start with :LspStart
  95. autostart = false,
  96. cmd = cmd,
  97.  
  98. --[[
  99. another important thing to know is the concept of the root directory in lspconfig and how it affects its behavior
  100.  
  101. By default lspconfig tries to guess the root directory of a repo when opening a source file
  102. if there already is a language server running with that root_dir and a matching language
  103. then it attaches the file to the existing language server instead of starting a new server for that file
  104.  
  105. For csharp_ls the default config is to walk up the file tree until you find a solution (*.sln)
  106. and if it can't find a solution, then walk up the file until it find a project (*.csproj)
  107. and if it can't find that if falls back to the current working directory
  108.  
  109. However sometimes solutions include projects and their source code that are not within the parent directory of the solution
  110. So lsp-config would guess a different root_dir and think the source file belongs to a different repo,
  111. which it has to start a second language server for
  112.  
  113. To make sure that doesn't happen, my root_dir callback prompts the user for the root dir,
  114. and once set, always returns that one root_dir
  115. it presets the root_dir it finds by default
  116.  
  117. This setup prevents loading up two different c# langauge servers within one nvim instance
  118. which to me isn't an issue. If I need to open another separate repo, I can start a second nvim instance altogether
  119.  
  120. Note that the Solution file can't be prompted dynamically in this way,
  121. since it has to be hard-coded into the cmd that starts the language sever during the time that lspconfig is configured
  122. which is when nvim starts up
  123.  
  124. --]]
  125. root_dir = function(startpath)
  126. if vim.g.root_dir ~= nil then
  127. return vim.g.root_dir
  128. end
  129.  
  130. local util = lspconfig.util
  131. local resultPath = util.root_pattern("*.sln")(startpath) or util.root_pattern('*.csproj')(startpath) -- default csharp_ls root_dir func
  132. if resultPath and resultPath == vim.g.root_dir then
  133. return resultPath
  134. end
  135. if resultPath == nil then
  136. resultPath = vim.fn.getcwd()
  137. end
  138. resultPath = vim.fn.input { prompt = "Project Root > ", default = resultPath, cancelreturn = resultPath }
  139. vim.g.root_dir = resultPath
  140. return resultPath;
  141. end,
  142. },
  143. }
  144. -- the rest if from the kickstart.nvim lsp setup
  145.  
  146. require('mason').setup()
  147.  
  148. -- You can add other tools here that you want Mason to install
  149. -- for you, so that they are available from within Neovim.
  150. local ensure_installed = vim.tbl_keys(servers or {})
  151. vim.list_extend(ensure_installed, {
  152. 'stylua', -- Used to format lua code
  153. })
  154. require('mason-tool-installer').setup { ensure_installed = {} }
  155. require('mason-lspconfig').setup {
  156. handlers = {
  157. function(server_name)
  158. local server = servers[server_name] or {}
  159.  
  160. -- This handles overriding only values explicitly passed
  161. -- by the server configuration above. Useful when disabling
  162. -- certain features of an LSP (for example, turning off formatting for tsserver)
  163. server.capabilities = vim.tbl_deep_extend('force', {}, capabilities, server.capabilities or {})
  164. require('lspconfig')[server_name].setup(server)
  165. end,
  166. },
  167. }
  168. end,
  169. },
  170.  
  171. -- this enabled decomiple goto-definition on nuget packages and corelib types
  172. -- note that this won't work under .NET framework for stdlib types.
  173. -- It just finds reference assemblies for those that don't contain the actual code
  174. -- it should still work for nuget packages and manuall assembly references under .NET framework
  175. -- as long as the assemlby refernces in the project file have their paths specified
  176. -- note that you will need to set a separate keybind for the decomp-goto-defintion command
  177. 'Decodetalkers/csharpls-extended-lsp.nvim',
  178.  
  179. -- ..........rest of plugins here
  180.  
  181. ```
  182.  
  183. ### Manually attaching buffers to an existing language server
  184.  
  185. If you already started your langauge server, and the file you have currently open
  186. wasn't attached to the server, (for example if it isn't under the root_dir),
  187. but you know it belongs to the same project, then you can force it to attach with the following command
  188.  
  189. :lua vim.lsp.buf_attach_clien(0, 1)
  190.  
  191. The first arg is the id of the buffer, with 0 always being the current buffer
  192. The second arg is the id of the language server, if you have started csharp_ls before any other language server
  193. then it should 1. But you can manually check which id it has with :LspInfo
  194.  
  195. ### Working with multiple Target-Frameworks and csharp-ls
  196.  
  197. I ran into that issue just recently as I have started migrating the code base I
  198. work on to .NET 8.
  199.  
  200. My workaround for that was to dynamically set a single target framework accross projects
  201.  
  202. ```csproj
  203. <Project Sdk="Microsoft.NET.Sdk">
  204. <PropertyGroup>
  205. <TargetFramework>$(dynamic_tf)</TargetFramework>
  206. // rest of csproj
  207.  
  208. ```
  209.  
  210. which is controlled via an external enviroment variable and a central `Directory.build.props`
  211. at the root of the repo. Which is imported into all projects automatically thanks to its naming scheme
  212.  
  213. And it contains this:
  214. ```props
  215. <Project>
  216. <PropertyGroup>
  217. <!-- this defines a custom Property within the msbuild system
  218. which can be evaluated for any other valur with the $() syntax
  219. -->
  220. <dynamic_tf>net48</dynamic_tf>
  221. </PropertyGroup>
  222.  
  223. <!-- msbuild projects are evaluted from top to bottom so this PropertyGroup overrides the value of dynamic_tf
  224. however it is only evaluated if the condition passes
  225. enviroment variables are also automatically set as properties so setting a TARGET_DOTNET_CORE enviroment variable with the value TRUE
  226. enables this PropertyGroup and therefore overrides the dynamic_tf variable
  227. -->
  228.  
  229. <PropertyGroup Condition=" '$(TARGET_DOTNET_CORE)' == 'TRUE' ">
  230. <dynamic_tf>net8.0-windows</dynamic_tf>
  231. </PropertyGroup>
  232.  
  233. // rest Directory.Build.props if any
  234. </Project>
  235. `
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement