omnisharp-roslyn: Semantic tokens do not conform to the LSP specification

Semantic highlighting was recently merged into Neovim. Unfortunately the token types provided by Omnisharp don’t conform to the LSP semantic tokens specification so Neovim cannot support it (neovim/neovim#21391).

To demonstrate the issue, here’s a comparison of Lua Language Server’s tokens to Omnisharp’s tokens. Omnisharp’s tokens are fundamentally wrong as well as having illegal formatting.

Lua Language Server (correct tokens)
{
 full = true,
 legend = {
    tokenModifiers = {
       "declaration",
       "definition",
       "readonly",
       "static",
       "deprecated",
       "abstract",
       "async",
       "modification",
       "documentation",
       "defaultLibrary",
       "global",
    },
    tokenTypes = {
       "namespace",
       "type",
       "class",
       "enum",
       "interface",
       "struct",
       "typeParameter",
       "parameter",
       "variable",
       "property",
       "enumMember",
       "event",
       "function",
       "method",
       "macro",
       "keyword",
       "modifier",
       "comment",
       "string",
       "number",
       "regexp",
       "operator",
       "decorator",
    },
 },
 range = true,
}
Omnisharp-roslyn (incorrect Tokens)
{
 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,
}

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 61
  • Comments: 33 (1 by maintainers)

Commits related to this issue

Most upvoted comments

@sid-6581 any chance you can add the hacky workaround here?

Sure! In my LSP handler on_attach I added the following:

  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

Basically just replacing the invalid tokens with valid ones. Not very future proof if that list changes, obviously.

You are in fact wrong, omnisharp doesn’t conform to Microsoft’s own LSP spec, how ironic 😆

Switched to csharp-language-server after this whole mess, no complaints on my end so far

Sure! In my LSP handler on_attach I added the following:

Basically just replacing the invalid tokens with valid ones. Not very future proof if that list changes, obviously.

We can make it a little bit more future proof:

  if client.name == 'omnisharp' then
    local tokenModifiers = client.server_capabilities.semanticTokensProvider.legend.tokenModifiers
    for i, v in ipairs(tokenModifiers) do
      tokenModifiers[i] = v:gsub(' ', '_')
    end
    local tokenTypes = client.server_capabilities.semanticTokensProvider.legend.tokenTypes
    for i, v in ipairs(tokenTypes) do
      tokenTypes[i] = v:gsub(' ', '_')
    end
  end

Attempted to fix it in #2520 but my C# isn’t great 😉

Ok I merged 2 workarounds* (@ateoi & @daephx ) and it seems to be working well:

vim.api.nvim_create_autocmd("LspAttach", {
  callback = function(ev)
    local client = vim.lsp.get_client_by_id(ev.data.client_id)
    local function toSnakeCase(str)
      return string.gsub(str, "%s*[- ]%s*", "_")
    end

    if client.name == 'omnisharp' then
      local tokenModifiers = client.server_capabilities.semanticTokensProvider.legend.tokenModifiers
      for i, v in ipairs(tokenModifiers) do
        tokenModifiers[i] = toSnakeCase(v)
      end
      local tokenTypes = client.server_capabilities.semanticTokensProvider.legend.tokenTypes
      for i, v in ipairs(tokenTypes) do
        tokenTypes[i] = toSnakeCase(v)
      end
    end
  end,
})

when I see the stars of Omnisharp git repository, it may be so remarkable. Now I changed my opinion.

Ok I merged 2 workarounds* (@ateoi & @daephx ) and it seems to be working well:

vim.api.nvim_create_autocmd("LspAttach", {
  callback = function(ev)
    local client = vim.lsp.get_client_by_id(ev.data.client_id)
    local function toSnakeCase(str)
      return string.gsub(str, "%s*[- ]%s*", "_")
    end

    if client.name == 'omnisharp' then
      local tokenModifiers = client.server_capabilities.semanticTokensProvider.legend.tokenModifiers
      for i, v in ipairs(tokenModifiers) do
        tokenModifiers[i] = toSnakeCase(v)
      end
      local tokenTypes = client.server_capabilities.semanticTokensProvider.legend.tokenTypes
      for i, v in ipairs(tokenTypes) do
        tokenTypes[i] = toSnakeCase(v)
      end
    end
  end,
})

Works well, don’t forget to place this code to load after lsp.

@natanncosta The on_attach function can be defined anywhere in your init.lua. It just needs to be passed to lspconfig["omnisharp"].setup({on_attach = on_attach}).

You could get similar results using the LspAttach autocmd.

The fix didn’t work for me so I just turned off semantic tokens for the time being.

vim.api.nvim_create_autocmd("LspAttach", {
  desc = "Fix startup error by disabling semantic tokens for omnisharp",
  group = vim.api.nvim_create_augroup("OmnisharpHook", {}),
  callback = function(ev)
    local client = vim.lsp.get_client_by_id(ev.data.client_id)
    if client.name == "omnisharp" then
      client.server_capabilities.semanticTokensProvider = nil
    end
  end,
})

@sid-6581 do you have to set up custom highlighting for new tokens? I was not sure and just remapped everything to a standard ones.

client.server_capabilities.semanticTokensProvider.legend = {
  tokenModifiers = { "static" },
  tokenTypes = { "comment", "excluded", "identifier", "keyword", "keyword", "number", "operator", "operator", "preprocessor", "string", "whitespace", "text", "static", "preprocessor", "punctuation", "string", "string", "class", "delegate", "enum", "interface", "module", "struct", "typeParameter", "field", "enumMember", "constant", "local", "parameter", "method", "method", "property", "event", "namespace", "label", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "xml", "regexp", "regexp", "regexp", "regexp", "regexp", "regexp", "regexp", "regexp", "regexp" }
}

If it can be solved with what is discussed above, I think it is a problem with the neovim/nvim-lspconfig plugin, not omnisharp. Am I wrong?

As a follow-up to @ateoi’s workaround, here is what I use to override my “global” on_attach function with one for OmniSharp that fixes the tokens:

lspcfg.omnisharp.setup({
  cmd = { "C:/Users/bakkenl/scoop/shims/OmniSharp.exe", "--languageserver", "--hostPID", tostring(pid) },
  capabilities = lsp_caps,
  root_dir = lspcfg_util.find_git_ancestor,
  on_attach = function (client, bufnr)
    -- https://github.com/OmniSharp/omnisharp-roslyn/issues/2483#issuecomment-1492605642
    local tokenModifiers = client.server_capabilities.semanticTokensProvider.legend.tokenModifiers
    for i, v in ipairs(tokenModifiers) do
      tmp = string.gsub(v, ' ', '_')
      tokenModifiers[i] = string.gsub(tmp, '-_', '')
    end
    local tokenTypes = client.server_capabilities.semanticTokensProvider.legend.tokenTypes
    for i, v in ipairs(tokenTypes) do
      tmp = string.gsub(v, ' ', '_')
      tokenTypes[i] = string.gsub(tmp, '-_', '')
    end
    on_attach(client, bufnr)
  end,
  flags = {
    debounce_text_changes = 150,
  }
})

@sid-6581 any chance you can add the hacky workaround here?

Sure! In my LSP handler on_attach I added the following:

  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

Basically just replacing the invalid tokens with valid ones. Not very future proof if that list changes, obviously.

Hm, this workaround doesn’t seem to work anymore. Maybe something changed or was added. Could you explain how you made this workaround list? I don’t see any of those tokens in the official doc.

Switched to csharp-language-server after this whole mess, no complaints on my end so far

For me it seems to have a lot of diagnostic which are missing. Do you not need them or did you find another solution?

No, I had that too but I don’t write much C# code so it wasn’t a huge issue for me. Ideally omnisharp-roslyn would fix this issue and conform to proper semantic token naming.