No description
Find a file
2025-09-17 13:13:30 +02:00
README.md update readme 2025-09-17 13:13:30 +02:00

Neovim + C#: formatting. | csharpier, conform.nvim

What do we need to get formatting in neovim?

  • A set of rules on how we want the formatting to look like
  • Executable that takes text and gives us formatted text based on these rules
  • lua code to integrate that executable into neovim
  • A key mapping in neovim to format the current buffer

The .editorconfig

"EditorConfig helps maintain consistent coding styles for multiple developers
working on the same project across various editors and IDEs."

A very simple .editorconfig can look like this

root = true

# All files
[*]

charset = utf-8-bom
end_of_line = crlf
indent_size = 4
indent_style = space
insert_final_newline = false
max_line_length = 50
tab_width = 4

# Xml files
[*.xml]
indent_size = 2

The Executable

The problem with dotnet format

This one will format correctly

                 Console.WriteLine("Hello, World!");

But when we run dotnet format over our Program.cs, it doesn't break those lines, even though max_line_length is set to 50

Console.WriteLine("Hello, World!"); Console.WriteLine("Hello, World!"); Console.WriteLine("Hello, World!"); Console.WriteLine("Hello, World!");

The csharp LSPs and dotnet format do not fully apply our .editorconfig - this is a long running issue: https://github.com/dotnet/roslyn/issues/15406

  • max_line_length is a property in the .editorconfig. The EditorConfig is a general concept, not csharp or dotnet specific.

  • dotnet format simply ignores max_line_length so jetbrains rider, vs code and visual studio all have individual implementations for this.

  • If you use something else than the above mentioned mainstream editors, then you need something else for proper formatting of max_line_length

This is where csharpier comes in

Correct .editorconfig formatting with csharpier

You can install csharpier easily with mason:

:MasonInstall csharpier

In order for csharpier to work, make sure the latest dotnet is installed (9.0.305 currently)

otherwise :ConformInfo will tell you "No frameworks were found."

Do it as mentioned here or simply copy and paste this code:

curl -fsSL -o ~/Downloads/dotnet-install.sh https://dot.net/v1/dotnet-install.sh && \
chmod +x ~/Downloads/dotnet-install.sh && \
cd ~/Downloads/ && \
sudo ./dotnet-install.sh --install-dir /usr/share/dotnet -channel STS -version 9.0.305

conform.nvim

What is conform.nvim

conform.nvim is the lua "glue" that connects the formattter with the currently opened neovim buffer. It works like so.

nvchad comes with a conform.nvim configuration by default

-- lua/plugins/init.lua

{
    "stevearc/conform.nvim",
    -- event = 'BufWritePre', -- uncomment for format on save
    config = function()
      require "configs.conform"
    end,
  }

Setting up csharpier in conform

The following lines make conform use csharpier to format .cs and .csproj files

-- lua/configs/conform.lua

local opts = {
  async = true,
  formatters_by_ft = {
    cs = { "csharpier_ramboe" },
    csproj = { "csharpier_ramboe" }
  },
  formatters = {
    csharpier_ramboe = {
      command = "csharpier",
      args = {
        "format",
        "--write-stdout",
      },
      to_stdin = true,
    },
  },
  -- format_on_save = {
  --   -- These options will be passed to conform.format()
  --   timeout_ms = 500,
  --   lsp_fallback = true,
  -- },
}

require("conform").setup(opts)

Conform passes the contents of the current buffer to the formatter through stdin and expects the formatted result back on stdout. Thats why we set to_stdin = true in the config and add --write-stdout to the csharpier command. Without this handshake, csharpier would just rewrite files on disk, but with stdin/stdout it behaves like a pipe:

[Neovim buffer] → stdin → csharpier → stdout → [formatted buffer]

Formatting the Current Buffer

NvChad

The default nvchad keymap for formatting the current buffer can be found here and looks like this:

map({ "n", "x" }, "<leader>fm", function()
  require("conform").format { lsp_fallback = true }
end, { desc = "general format file" })

Demo: csharpier formatting

After setting up csharpier, our Program.cs should now look like this

Console.WriteLine("Hello, World!");
Console.WriteLine("Hello, World!");
Console.WriteLine("Hello, World!");
Console.WriteLine("Hello, World!");

Formatting .razor files

dotnet format and prettier both do not apply to .razor files. We have to rely on LSP formatting.

Create Test project

cd ~/Documents && \
dotnet new blazor -n MyBlazor
cd MyBlazor && \
dotnet build

Enable good enough formatting

A look at the rzls.nvim formatting dependencies reveals: all we need to do is having html-lsp installed.

In the moment conform.nvim recognizes there is no configuration for .razor files, it wall fall back to rzls.nvim which then uses html-lsp to format our file

:MasonInstall html-lsp

What did we accomplish?

  • A set of rules on how we want the formatting to look like - .editorconfig
  • Executable that takes text and gives us formatted text based on these rules - csharpier
  • lua code to integrate that executable into neovim - conform.nvim
  • A key mapping in neovim to format the current buffer - <leader>fm

What's next?

Unfortunately...

html-lsp itself does not expose a setting to wrap attributes vertically. It wont turn

<button class="btn btn-primary" some="more" @onclick="IncrementCount">Click me</button>

into

<button class="btn btn-primary"
        some="more"
        @onclick="IncrementCount">
  Click me
</button>

There is a plugin that potentially can do that but I couldn't get it to work and the last commit has been 4 years ago

But maybe there is hope

JetBrains offers MIT licensed CLIs

Upsides

  • razor support
  • rider colleagues are happy now

Downsides

  • potentially slow
  • works on the file directly, no stdin/stdout support