Search code examples
c#windowsneovimomnisharpnvim-lspconfig

Omnisharp C# LSP with Neovim on Windows


As the title says I need help with setting up C# LSP with Omnisharp. I have already installed the latest version. I downloaded omnisharp-win-x64.zip from the releases page and extracted it to a custom directory.

I have nvim-lspconfig installed and this is what I have in my config.

for _, lsp in ipairs(servers) do
  if vim.loop.os_uname().sysname == "Windows_NT" and lsp == "omnisharp" then
    local pid = vim.fn.getpid()
    local omnisharp_bin = "C:\\Users\\yapji\\lsp\\omnisharp\\OmniSharp.exe"
    lspconfig[lsp].setup {
      handlers = handlers,
      on_attach = on_attach,
      capabilities = capabilities,
      cmd = { omnisharp_bin, "--languageserver", "--hostPID", tostring(pid) },
    }
  else
    lspconfig[lsp].setup {
      handlers = handlers,
      on_attach = on_attach,
      capabilities = capabilities,
    }
  end
end

I looked more into the markdown file which talks about setting up omnisharp can be read here and I think I found out the problem. As stated in the file this line specifically.

By default, omnisharp-roslyn doesn't have a cmd set. This is because nvim-lspconfig does not make assumptions about your path. You must add the following to your init.vim or init.lua to set cmd to the absolute path ($HOME and ~ are not expanded) of the unzipped run script or binary.

I need to set the cmd property. But looking through the omnisharp file there is no cmd property. The reason I have it in mine is because I followed this guide and I did my best to adapt it to Windows but like I mentioned it didn't work. If more information is needed, I will update the question which it. Thanks. I've also found this answer but there aren't any answers with explicit neovim support and I'll leave that for last.

I installed omnisharp from the releases page and extracted it and setup my neovim config as shown above.


Solution

  • You have to define cmd property, otherwise neovim won't know how to launch omnisharp language server, this is correct.

    Couple of things that could go out of place, and that should be checked:

    • I'm not a lua expert, but check if path slashes ("") are escaped properly. I'm personally using environment variables to define path to language servers (I use my configuration across various windows\linux machines) it goes like this:

      local os = require('os')
      local omnisharp_server_location = os.getenv('OMNISHARP_LANGUAGE_SERVER')
      ...
      ...
      -- somewhere in the code where server configuration is defined
      require('lspconfig').omnisharp.setup({
          on_attach = on_attach,
          capabilities = capabilities,
          cmd = { omnisharp_server_location, "--languageserver" , "--hostPID", tostring(pid) },
       })
      
    • As far as I know there are couple distributions of omnisharp, make sure that you are using correct architecture (arm64\x64\x86), try to execute it manually from cmd and see if there are any unwanted failures. As I've mentioned in the comment, check lsp server logs here: %USERPROFILE%\AppData\Local\nvim-data\lsp.log

    • Make sure that your configuration is actually set, check :LspInfo with csharp file opened in neovim.

    • One more important thing, there is a bug in omnisharp server (or in the roslyn itself), here are opened issues:

    Bottomline

    Here is an example of minimalistic omnisharp configuration in neovim, so you can play with it, just copy it to a separate file and launch neovim with -u flag like this:

    nvim -u /path/to/the/config.lua
    

    It has packer bootstrapped so you'll have to run it twice, and run :PackerSync to install needed plugins.

    Configuration:

    local os = require('os')
    local fn = vim.fn
    
    -- bootstrapping packer for simplicity of the example
    -- packer installation bootstrapping
    local install_path = fn.stdpath('data')..'/site/pack/packer/start/packer.nvim'
    if fn.empty(fn.glob(install_path)) > 0 then
      packer_bootstrap = fn.system({'git', 'clone', '--depth', '1', 'https://github.com/wbthomason/packer.nvim', install_path})
    end
    
    -- bare minimum for the demo installation
    require('packer').startup(function(use)
      use 'neovim/nvim-lspconfig' -- language server configurations
      use 'hrsh7th/nvim-cmp' -- autocompletion framework
      use 'hrsh7th/cmp-nvim-lsp' -- LSP autocompletion provider
    
      if packer_bootstrap then
        require('packer').sync()
      end
    end)
    
    
    -- autocomplete setup
    vim.cmd [[
        set completeopt=menu,menuone,noselect
    ]]
    
    local cmp = require('cmp')
    cmp.setup({
      mapping = {
        ['<Tab>'] = cmp.mapping.select_next_item(),
        ['<S-Tab>'] = cmp.mapping.select_prev_item(),
        ['<CR>'] = cmp.mapping.confirm({
          behavior = cmp.ConfirmBehavior.Replace,
          select = true,
        })
      },
      sources = cmp.config.sources({
          { name = 'nvim_lsp' }, 
      }),
    })
    
    
    local capabilities = require('cmp_nvim_lsp')
        .default_capabilities(vim.lsp.protocol.make_client_capabilities())
    
    
    local on_attach = function(client, bufnr)
        -- temporary fix for a roslyn issue in omnisharp
        -- opened issues:
        -- https://github.com/OmniSharp/omnisharp-roslyn/issues/2483
        -- https://github.com/neovim/neovim/issues/21391
        if client.name == "omnisharp" then
            client.server_capabilities.semanticTokensProvider = {
              full = vim.empty_dict(),
              legend = {
                tokenModifiers = { "static_symbol" },
                tokenTypes = {
                  "comment",
                  "excluded_code",
                  "identifier",
                  "keyword",
                  "keyword_control",
                  "number",
                  "operator",
                  "operator_overloaded",
                  "preprocessor_keyword",
                  "string",
                  "whitespace",
                  "text",
                  "static_symbol",
                  "preprocessor_text",
                  "punctuation",
                  "string_verbatim",
                  "string_escape_character",
                  "class_name",
                  "delegate_name",
                  "enum_name",
                  "interface_name",
                  "module_name",
                  "struct_name",
                  "type_parameter_name",
                  "field_name",
                  "enum_member_name",
                  "constant_name",
                  "local_name",
                  "parameter_name",
                  "method_name",
                  "extension_method_name",
                  "property_name",
                  "event_name",
                  "namespace_name",
                  "label_name",
                  "xml_doc_comment_attribute_name",
                  "xml_doc_comment_attribute_quotes",
                  "xml_doc_comment_attribute_value",
                  "xml_doc_comment_cdata_section",
                  "xml_doc_comment_comment",
                  "xml_doc_comment_delimiter",
                  "xml_doc_comment_entity_reference",
                  "xml_doc_comment_name",
                  "xml_doc_comment_processing_instruction",
                  "xml_doc_comment_text",
                  "xml_literal_attribute_name",
                  "xml_literal_attribute_quotes",
                  "xml_literal_attribute_value",
                  "xml_literal_cdata_section",
                  "xml_literal_comment",
                  "xml_literal_delimiter",
                  "xml_literal_embedded_expression",
                  "xml_literal_entity_reference",
                  "xml_literal_name",
                  "xml_literal_processing_instruction",
                  "xml_literal_text",
                  "regex_comment",
                  "regex_character_class",
                  "regex_anchor",
                  "regex_quantifier",
                  "regex_grouping",
                  "regex_alternation",
                  "regex_text",
                  "regex_self_escaped_character",
                  "regex_other_escape",
                },
              },
              range = true,
            }
        end
    
        -- specifies what to do when language server attaches to the buffer
        vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')
    end
    
    
    local omnisharp_server_location = os.getenv('OMNISHARP_LANGUAGE_SERVER')
    
    local pid = vim.fn.getpid()
    require('lspconfig').omnisharp.setup({
      on_attach = on_attach,
      capabilities = capabilities,
      cmd = { omnisharp_server_location, "--languageserver" , "--hostPID", tostring(pid) },
    })
    
    

    Update

    Since version 1.39.8 of omnisharp, there is no more need for LSP token types workaround in the configuration. It was fixed.