Hidden features of OmniSharp and C# extension for VS Code

Β· 1036 words Β· 5 minutes to read

OmniSharp powers intellisense and language services in C# plugins and extensions for numerous editors, including VS Code. When we build things into OmniSharp, we typically try to keep things lightweight (of course if the term “lightweight” applies to anything related to MSBuild…) and non-invasive. This means that many features/tweaks are actually opt-in by default, and wouldn’t normally show up on their own.

In this post I wanted to show you a few of such less known features.

OmniSharp options πŸ”—

Typically, tweaking different OmniSharp features or settings happens via OmniSharp options, and the linked document describes in detail the process that OmniSharp uses to discover its configuration. To not make this article unnecessarily complicated, we can summarize the whole thing with the following:

  • for global settings, use %USERPROFILE%/.omnisharp/omnisharp.json file
  • for project specific settings, use omnisharp.json at the root of your workspace (typically at the root of the repository, next to solution file)

If the file doesn’t exist, just create it. The file is normally also respected in real time too, so any changes should be reflected without having to restart the editor.

Specifically in C# extension in VS Code, for some settings, there is also a special shorthand syntax that allows you to enable some stuff directly through VS Code settings and then VS Code would feed this into OmniSharp at startup (more on that later).

Warm up - analyzers and .editorconfig πŸ”—

As a warm up, I wanted to remind everyone that both Roslyn analyzers and .editorconfig file are supported in OmniSharp, they just have to be enabled via the config, as they are switched off by default.

I blogged about both of those earlier - analyzers post, editorconfig post - you can follow these links to learn more about these features. Here we will only mention that they can be enabled by adding the following settings to your omnisharp.json file:

{
    "RoslynExtensionsOptions": {
        "enableAnalyzersSupport": true
    },
    "FormattingOptions": {
        "enableEditorConfigSupport": true
    }
}

As I briefly mentioned, some of the OmniSharp features have a shortcut directly via the standard VS Code settings. That is in fact the case for both of the above, and as such, you can also enable them that way:

"omnisharp.enableEditorConfigSupport": true,
"omnisharp.enableRoslynAnalyzers": true

The advantage of using omnisharp.json, however, is that you could commit that file to source control and other developers working on your project would get OmniSharp to behave the same way as on your machine.

Renaming options πŸ”—

Just like Visual Studio, OmniSharp has support for more sophisticated symbol renaming - renaming a symbol can propagate to comments or strings, and renaming a method symbol can also rename its overloads.

The configuration options are as follows:

{
    "RenameOptions": {
        "RenameInComments": true,
        "RenameOverloads": true,
        "RenameInStrings": true
    }
}

The practical consequences are the following.

  1. Comments
using System;
namespace ConsoleApplication
{
    /// <summary>  
    ///  This program performs an important work and calls Bar.  
    /// </summary> 
    public class Program
    {
        static void Bar() {}
    }
}";

Renaming the Bar() method in the snippet above, would propagate the rename into the comment that sits over the Program class.

  1. Overloads
public class Foo
{
    public void DoStuff() {}
    
    public void DoStuff(int foo) {}
    
    public void DoStuff(int foo, int bar) {}
}";

Renaming any of the DoStuff overloads from the example above, would rename all of its “siblings” too. The rename is of course done over the semantic model and all usages of all the overloads are properly adjusted too.

  1. Strings
namespace ConsoleApplication
{
    public class Bar
    {
        public static string Name = "Bar";
    }
}

Here, renaming the class Bar would also rename the static string Bar.

Organize imports on formatting πŸ”—

When running code formatting, OmniSharp can automatically organize your imports. This doesn’t mean it would remove unused usings, there is a separate code fix for that, and the feature operates on raw syntax trees only - but they would get sorted.

You can enable it the following way via omnisharp.json:

{
    "FormattingOptions": {
        "OrganizeImports": true
    }
}

With this switched on, as soon as you invoke formatting in the editor, the imports will be organized in a way that System directives will be placed first, and all other ones will be alphabetically sorted afterwards.

At the moment the feature itself is not customizable any further - meaning you cannot select the rules that are used for import organization. In the future we will add the possibility to specify spacing between groups and whether System directives should come first or not - those are the “knobs” exposed by the Roslyn compiler.

For some background, this import organization feature was actually added to Roslyn public API surface to be consumed in the dotnet format tool, and we benefited from that in OmniSharp.

Type implementation options πŸ”—

This is a neat one, that solves quite an annoying default behavior. It impacts how code actions generate code and consists of two parts:

  • controlling the order in which newly generated members should be added to a type
  • defining whether newly generated properties should be auto-properties or have bodies that throw NotImplementedException

The annoying behavior - in my book - is our default behavior for newly generated properties which are always created as throwing properties; I personally think that in overwhelming majority of cases users want auto properties instead. However, since Visual Studio default is also to generate throwing properties, we decided to keep that as a consistent default in OmniSharp too.

That said, the settings look as follows, in each case you can pick one of the two values:

{
    "ImplementTypeOptions": {
        "PropertyGenerationBehavior": "PreferAutoProperties|PreferThrowingProperties",
        "InsertionBehavior": "AtTheEnd|WithOtherMembersOfTheSameKind"
    }
}

We discussed briefly the PropertyGenerationBehavior. The second setting, InsertionBehavior behavior allows you to define that new members are generated AtTheEnd of the type - as the default behavior would be to group them by member kind (i.e. properties together).

Summary πŸ”—

OmniSharp tries to have reasonable, stable default settings, and doesn’t attempt to throw lots of complexity at you from the beginning. However, it has quite a few knobs and settings that may not be generally known, that can help make your C# experience a bit more pleasant.

If you are interested in more “insider” OmniSharp tips and tricks let me know and we could cook up another post or two in the future.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP