How to fully configure Neovim for Unity

Waisted a weeks trying to make unity and neovim work together, so here is instructions on how to make debugging, file opening and opening file in existing neovim instance work.

I use windows but only differences is to create sh script that does the same as my bat does.

Opening files with neovim

There are instruction that suggest using .bat file directly, but this didn’t worked for so we will use bat file indirectly. i.e. we will be executing terminal.exe that will execute bat file.

Setting windows terminal (if you are using it, else skip this step)

  1. Set windows terminal as your default terminal, or else it may open legacy terminal. Also set powershell as default (optional)

  2. Also set nerd font as a default for all profiles

  3. Find windows terminals executable it is located in

    C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_(someversion)\WindowsTerminal.exe

    you won’t be able to open WindowsApps folder here is how to get access to it
    source

    Right-click the WindowsApps folder > select "Properties" > switch the "Security" tab > click the "Advanced" button > click "change" > select the "Advanced" button on the User Name and Group screen > select "Find Now" > find "everyone" and double-click it > ok > Apply > Ok

    Image

    Image

Setting terminal as external tool in Unity

In Unity go to Edit → Preferences → External Tools

Click on External Script Editor then Browse and find your terminals exe file

In External Script Editor Args enter path to bat file that we will create and $(File) +$(Line) (no space after + sign!)

Our bat will start nvim and pass it with filepath and line number to it.

Bat script

We can just write nvim &* and it will work, but!

The issue is that if you do so it will open new instance of nvim every time, so if you already have nvim opened it will just open new one. Instead what we want is if there is already nvim opened then open file in it else open new nvim.

To achieve this we need nvr cli https://github.com/mhinz/neovim-remote (just pip install it)

Then we need nvim to always open same server, or else we will need to provide server address to nvr every time and we can’t do this in bat script.

To do so we need to either manually pass server address nvim --listen "127.0.0.1:6789" every time we open nvim. but I don’t want to do this, so in my profile.ps1
(powershells user start up script that runs everytime you open ps. located in ~\documents\powershell\profile.ps1)
I added custom function

function nvims {
    nvim.exe --listen "127.0.0.1:6789" $args
}
# for linux it's probably --listen /tmp/nvimsocket

So now I have nvims command that I can run and it will have a specific server address, you can just call function nvim but then you won’t be able to open multiple nvim instances if you will want to.

now our final bat files is

@echo off
nvr -s --nostart %*
if %errorlevel% neq 0 (
    nvim --listen "127.0.0.1:6789" %*
)

We call nvr that send request to existing nvim instance with file name and line number.
if nvr returns error this means there isn’t existing nvim session, so it will open new one with specified server

(-s for silent, —nostart so it won’t try to start nvim itself because it is unreliable in my experience
%* is our $(File) +$(Line) args)

Now we can open files in unity!

Debugger

I use LazyVim because I’m not yet on a custom nvim setup arc, but if you are you can figure it out. 😝

if you use lazyvim you need to open lazyextras and install dap.core, it will install: nvim-dap, nvim-dap-ui.
Also in lazyextras toggle lang.omnisharp, it will install omnisharp-extended-lsp.

omnisharp has a lot of rules that I don’t like to follow, so to disable them you can create .editorconfig file in root of project and add stuff like

[*.cs]
dotnet_diagnostic.IDE0040.severity = none
dotnet_diagnostic.IDE0008.severity = none

Important plugins!
And 'nagaohiroki/unity.nvim' for nvim to be able to attach to unity this is config for it

  {
    'nagaohiroki/unity.nvim',
    opts = {
        discover_time = 2000 -- default option
    }
  }

then in nvim run :InstallUnityDebugger

And this is config for nvim-dap

  {
    'mfussenegger/nvim-dap',
    keys = {
      {
        "<leader>dU",
        function()
          local dap = require('dap')
          if dap.session() == nil then
            local unity = require('unity')
            -- vstuc
            dap.adapters.vstuc = unity.vstuc_dap_adapter()
            dap.configurations.cs = unity.vstuc_dap_configuration()
          end
          dap.continue()
        end,
        desc = "Attach to Unity"
      },
    },
  },

this will give us ability to attach to unity process by clicking <leader>dU

Now you can trigger dap ui (for me it is <leader>du) add brakepoint (for me it is <leader>db) and then attach to unity (for me <leader>dU) then run your game and it should work!

Now it Basically works but there are two extra points!

Extra: New files not adding to csproj

There is one issue with new files now, they are not being added to csproj automatically, so you need to add them manually OR we can customize VS/VSCode external tool script to make it handle syncing while opening will be done by neovim.

Go to Library\PackageCache of your project and copy com.unity.ide.visualstudio folder to Packages folder in your project root next to Assets folder.

Then open VisualStudioEditor.cs file and add next to it

const string terminalExeKey = "nvim-terminal";
const string NvimExeKey = "nvim-bat";
const string ArgsKey = "nvim-args";

public void OnGUI()
{
    GUILayout.BeginHorizontal();
    GUILayout.FlexibleSpace();

    if (!TryGetVisualStudioInstallationForPath(CodeEditor.CurrentEditorInstallation, true, out var installation))
        return;

    var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(GetType().Assembly);

    var style = new GUIStyle
    {
        richText = true,
        margin = new RectOffset(0, 4, 0, 0)
    };

    GUILayout.Label($"<size=10><color=grey>{package.displayName} v{package.version} enabled</color></size>", style);
    GUILayout.EndHorizontal();

    # Only Difference from original OnGUI
    TextField("Arguments", ArgsKey);
    TextField("Nvim Bat", NvimExeKey);
    TextField("Terminal", terminalExeKey);
    #

    EditorGUILayout.LabelField("Generate .csproj files for:");
    EditorGUI.indentLevel++;
    SettingsButton(ProjectGenerationFlag.Embedded, "Embedded packages", "", installation);
    SettingsButton(ProjectGenerationFlag.Local, "Local packages", "", installation);
    SettingsButton(ProjectGenerationFlag.Registry, "Registry packages", "", installation);
    SettingsButton(ProjectGenerationFlag.Git, "Git packages", "", installation);
    SettingsButton(ProjectGenerationFlag.BuiltIn, "Built-in packages", "", installation);
    SettingsButton(ProjectGenerationFlag.LocalTarBall, "Local tarball", "", installation);
    SettingsButton(ProjectGenerationFlag.Unknown, "Packages from unknown sources", "", installation);
    SettingsButton(ProjectGenerationFlag.PlayerAssemblies, "Player projects", "For each player project generate an additional csproj with the name 'project-player.csproj'", installation);
    RegenerateProjectFiles(installation);
    EditorGUI.indentLevel--;
}

void TextField(string inLabel, string inKey, string inDefaultValue = "")
{
    EditorGUILayout.BeginHorizontal();
    EditorGUILayout.LabelField(inLabel);
    EditorPrefs.SetString(inKey, EditorGUILayout.TextField(GetString(inKey, inDefaultValue)));
    EditorGUILayout.EndHorizontal();
}

const string defaultExt = ".cs,.shader,.json,.xml,.txt,.yml,.yaml,.md";
public bool OpenProject(string filePath, int line, int column)
{
    if (!IsCodeAsset(filePath, defaultExt.Split(',')))
    {
        return false;
    }

    var nvim = EditorPrefs.GetString(NvimExeKey);

    var args = EditorPrefs.GetString(ArgsKey).
        Replace("$(File)", filePath).
        Replace("$(Line)", Mathf.Max(0, line).ToString()).
        Replace("$(Column)", Mathf.Max(0, column).ToString());

    var info = new System.Diagnostics.ProcessStartInfo
    {
        FileName = EditorPrefs.GetString(terminalExeKey),
        CreateNoWindow = false,
        UseShellExecute = false,
        Arguments = $"{nvim} {args}"
    };

    System.Diagnostics.Process.Start(info);

    return true;
}

This will add 3 input fields for Terminal, Nvim, and Args. And Replace OnGUI to render extra fields and OpenProject with will open Nvim instead of VS.

This is how it will work now

Extra: OmniSharp Performance issues

I was experiencing a lot of performance issues with OmniSharp, it would either make nvim extremely laggy or not work at all. There are three potential fixes (I use third one because first two weren’t working for me):

  1. Customize omnisharp.json located at ~\.omnisharp\omnisharp.json or create one file in your projects root directory, then add “AnalyzeOpenDocumentsOnly” this will make it only analyze file that is open

     {
       "RoslynExtensionsOptions": {
         "EnableAnalyzersSupport": true,
         "enableDecompilationSupport": true, 
         "AnalyzeOpenDocumentsOnly": true # only analyzes open file
       },
       "FormattingOptions": {
         "enableEditorConfigSupport": true # makes omnisharp honor .editorconfig config
       }
     }
    
  2. same as before create or customize omnisharp.json but now we enable “loadProjectsOnDemand” this should only load project that opened file belongs to.

     {
       "RoslynExtensionsOptions": {
         "enableAnalyzersSupport": false,
         "enableDecompilationSupport": true
       },
       "msbuild": {
         "loadProjectsOnDemand": true
       },
       "FormattingOptions": {
         "enableEditorConfigSupport": true
       }
     }
    
  3. Final fix, don’t use Omnisharp, use csharp_ls instead, it seems to work faster and has almost same set of features. To install it simply open :Mason install csharp_ls, and Uninstall and disable Omnisharp

         {
           "neovim/nvim-lspconfig",
           opts = {
             servers = {
               omnisharp = {
                 enabled = false,
               },
             },
           },
         },
    

    this will work but if you also want to go to definition with decomplication then you also will need to install Decodetalkers/csharpls-extended-lsp.nvim and edit lspconfig to use it when gd

     { "Decodetalkers/csharpls-extended-lsp.nvim" }, -- install csharpls-extended-lsp
     {
       "neovim/nvim-lspconfig",
       opts = {
         servers = {
           csharp_ls = {
             keys = {
               {
                 "gd",
                 function() -- use it
                   require("csharpls_extended").lsp_definitions()
                 end,
                 desc = "Goto Definition",
               },
             },
           },
           omnisharp = {
             enabled = false, -- disable omnisharp
           },
         },
       },
     },
    

Extra: Ignoring files

If you have issues because of a large amount of files in you project you can create file called .ignore with same syntax as .gitignore to ignore some files or folders from file and word searches, here is example of ignoring everything except cs files

*
!.*
!*.cs
!*/
.vs
.idea
.plastic
Assets/Plugins
Assets/Packages
Packages
Temp
Library

Good Luck, you will need it! 🫡

0
Subscribe to my newsletter

Read articles from Andrian Kogoshvili directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Andrian Kogoshvili
Andrian Kogoshvili