An open API service indexing awesome lists of open source software.

https://github.com/khoido2003/roslyn-filewatch.nvim

File watcher for Roslyn LSP in Neovim (C#)
https://github.com/khoido2003/roslyn-filewatch.nvim

csharp file-watcher neovim neovim-plugin nvim roslyn

Last synced: 3 months ago
JSON representation

File watcher for Roslyn LSP in Neovim (C#)

Awesome Lists containing this project

README

          


roslyn-status
checkhealth

# roslyn-filewatch.nvim

A lightweight, **file-watching and project-sync plugin** for Neovim that keeps the **Roslyn LSP** up-to-date with file changes.

Now with **Game Engine support**.

---

> [!WARNING]
> **v0.4.x Breaking Changes**: This release removes all non-file-watching features (Dotnet CLI, NuGet, Explorer) to focus on performance.
> If you need these features, please pin to `v0.3.9`. See [Migration Guide](#migration-guide-v04x) (Not recommended).

---

## Why?

Roslyn does not watch your project files by default in Neovim/Linux/Mac. Without this, you often need to `:edit!` or restart the LSP to make Roslyn notice file creation, deletion, renaming, or solution changes.

This plugin adds a robust **cross-platform file watcher**.

---

## Features

### Robust File Watching
- **Native Rust Acceleration**: Ships with a dynamically linked Rust backend (`roslyn_filewatch_rs`) utilizing the `ignore` crate for 1-3ms instantaneous native repository indexing.
- **Cross-Platform**: Uses Neovim 0.10+ API natively, with fallback to Native Watchers (`watchman`, `fswatch`) or Sparse Polling.
- **Smart Detection**: Handles create, delete, change, and **detects renames** (merging delete+create pairs).
- **Optimization**: Batches events, throttles diagnostics, and avoids watching ignored files (ignores `.git`, `bin`, `obj`, etc.).
- **Solution-Aware**: Parses `.sln`, `.slnx`, or `.slnf` files across isolated background OS threads (`vim.uv.new_work`) to completely eliminate UI blocking.
- **csproj-only Support**: Fully supports projects without solution files - automatically detects and watches all `.csproj` files recursively, ensuring new files are immediately recognized by the LSP.

### Performance & Smart Loading
- **Deferred Loading**: Delays project loading until you actually open a C# file to speed up startup for large solutions.
- **Project Warm-up**: Sends initialization notifications to get Roslyn ready without blocking the UI.
- **Diagnostic Throttling**: Prevents UI lag by smoothing out diagnostic updates during heavy operations (like git checkout).

### Game Engine Support
First-class support for **C# Game Development**.
Automatically detects the engine and applies optimized presets (scan intervals, ignore patterns):
- **Unity**: Parses `.asmdef`, configures analyzers, handles meta files.
- **Godot**: Handles `project.godot` and `.godot/`.
- **Stride**, **MonoGame**, **FNA**: Preset configurations included.

---

## Requirements

### Required

| Dependency | Minimum Version | Purpose |
|:---|:---|:---|
| **Neovim** | 0.10+ | Core editor (requires `vim.fs`, `vim.uv`, modern Lua APIs) |
| **Roslyn LSP client** | — | The C# language server this plugin integrates with |
| **.NET SDK** | 10.0+ | Needed for `dotnet restore` (auto-restore of NuGet packages) |

### Recommended (Performance)

These tools dramatically improve performance on large projects. **Without them, the plugin still works** but uses slower fallback mechanisms.

| Dependency | Purpose | Impact |
|:---|:---|:---|
| **Rust native module** | Fastest file scanning (1-3ms for entire repos) | 10-100x faster initial scans |
| **[fd](https://github.com/sharkdp/fd)** | Fast file finder (fallback when Rust unavailable) | 5-20x faster than pure Lua |
| **[Watchman](https://facebook.github.io/watchman/)** | Facebook's file watching service | Best for huge monorepos (10k+ files) |
| **[fswatch](https://github.com/emcrisostomo/fswatch)** | Cross-platform file change monitor | Good alternative to Watchman on macOS/Linux |

The plugin automatically selects the best available backend in this priority order:

```
Rust native → fd/fdfind → Pure Lua (slowest)
Watchman → fswatch → fs_event/polling
```

> [!TIP]
> Run `:checkhealth roslyn_filewatch` to see which scanning tier and watcher backend are active on your system.

---

## Dependency Installation

### Roslyn LSP Client

You need one of these Neovim Roslyn LSP integrations:

- **[roslyn.nvim](https://github.com/seblyng/roslyn.nvim)** (**Highly recommended**)
- **[nvim-lspconfig (roslyn_ls)](https://github.com/neovim/nvim-lspconfig/blob/master/doc/configs.md#roslyn_ls)**

### .NET SDK

Required for C# development and NuGet auto-restore.

| Platform | Command |
|:---|:---|
| **Windows** | `winget install Microsoft.DotNet.SDK.10` |
| **macOS** | `brew install dotnet-sdk` |
| **Ubuntu/Debian** | `sudo apt install dotnet-sdk-10.0` |
| **Fedora** | `sudo dnf install dotnet-sdk-10.0` |
| **Arch Linux** | `sudo pacman -S dotnet-sdk` |

Or download from [dotnet.microsoft.com/download](https://dotnet.microsoft.com/download).

### fd (Recommended)

Fast file finder used as fallback when the Rust native module is unavailable.

| Platform | Command |
|:---|:---|
| **Windows** | `winget install sharkdp.fd` or `scoop install fd` or `choco install fd` |
| **macOS** | `brew install fd` |
| **Ubuntu/Debian** | `sudo apt install fd-find` (binary is `fdfind`) |
| **Fedora** | `sudo dnf install fd-find` |
| **Arch Linux** | `sudo pacman -S fd` |

### Watchman (Recommended for large repos)

Best performance for monorepos with thousands of files and supported on all platforms.

| Platform | Command |
|:---|:---|
| **Windows** | `choco install watchman` or download from [GitHub releases](https://github.com/nicoster/watchman-bin/releases) |
| **macOS** | `brew install watchman` |
| **Ubuntu/Debian** | [Official install guide](https://facebook.github.io/watchman/docs/install#linux) (apt from Meta's repo) |
| **Arch Linux** | `yay -S watchman-bin` |

### fswatch (Optional, macOS/Linux)

Alternative to Watchman for Unix systems.

| Platform | Command |
|:---|:---|
| **macOS** | `brew install fswatch` |
| **Ubuntu/Debian** | `sudo apt install fswatch` |
| **Fedora** | `sudo dnf install fswatch` |
| **Arch Linux** | `sudo pacman -S fswatch` |

### Rust Toolchain (Recommended for building from source)

Only needed if you want to compile the native Rust module yourself instead of using pre-built binaries.

| Platform | Command |
|:---|:---|
| **All platforms** | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh` |
| **Windows** | Download from [rustup.rs](https://rustup.rs/) |

> [!NOTE]
> The build script (`build.lua`) automatically downloads pre-compiled binaries if `cargo` is not found. You only need Rust installed if you want to build from source or the prebuilt binary isn't available for your platform.

---

## Installation

#### Native Rust Backend (Highly Recommended)

> [!IMPORTANT]
> For the best performance on large repositories (like Unity games or monorepos with heavy `node_modules`), the plugin ships with a **Native Rust Snapshot Module**.
> The included `build.lua` script handles installation automatically:
> 1. If you have `cargo` installed locally, it compiles from source natively.
> 2. If you don't have Rust, it automatically uses `curl` to dynamically download a pre-compiled binary matching your exact OS directly from GitHub Releases.

### lazy.nvim
```lua
{
"khoido2003/roslyn-filewatch.nvim",
build = "nvim -l build.lua --", -- Compiles or downloads the Native Rust module fallback
config = function()
require("roslyn_filewatch").setup()
end,
}
```

### packer.nvim
```lua
use {
"khoido2003/roslyn-filewatch.nvim",
run = "nvim -l build.lua",
config = function()
require("roslyn_filewatch").setup()
end,
}
```

## Rebuilding the Native Module

If you need to manually trigger a rebuild (e.g. after updating the plugin):

### lazy.nvim
The module is built automatically on install. To rebuild manually:

```lua
:Lazy build roslyn-filewatch.nvim
```

### Or from your terminal
```sh
cd ~/.local/share/nvim/lazy/roslyn-filewatch.nvim
nvim -l build.lua
```

### rocks.nvim / other plugin managers
```sh
cd
nvim -l build.lua
```

> **Note:** Do not use `:lua dofile(...)` to trigger the build — it does not set the
> working directory correctly and may fail to locate the plugin root.

### Migration Guide (v0.4.x)

**Breaking Changes**: v0.4.x is a strict **File Watcher** and **Project Sync** tool. All "IDE-like" features (Building, Running, Nuget, Explorer) have been removed to minimize bloat and maximize performance.

| Feature | v0.3.x (Deprecated) | v0.4.x (Current) | Replacement |
| :--- | :--- | :--- | :--- |
| **Project Watching** | Good | **Instant** (Native Rust + Threading) | - |
| **Dotnet CLI** | `:RoslynBuild`, `:RoslynRun` | ❌ Removed | Use `dispatch.nvim` or `toggleterm` |
| **NuGet** | `:RoslynNuget` | ❌ Removed | Use CLI or `nuget.nvim` |
| **Explorer** | `:RoslynExplorer` | ❌ Removed | Use `neo-tree` or `nvim-tree` |
| **Game Detection** | Complex Context | **Presets Only** (Performance) | Presets are auto-applied |

**Recommendation:**
* **Stay on v0.4.x** if you want the fastest, newest Roslyn experience and stable releases.
* **Pin to v0.3.x** if you absolutely rely on the built-in CLI/Explorer commands (Not recommended):

```lua
{ "khoido2003/roslyn-filewatch.nvim", branch = "v0.3.x" }
```

---

## User Manual

This section covers how to configure and use the plugin effectively in your daily workflow.

### 1. Configuration

The following settings are commonly adjusted:

```lua
require("roslyn_filewatch").setup({
-- === Core Settings ===
-- List of LSP client names to hook into (required)
client_names = { "roslyn_ls", "roslyn", "roslyn_lsp" },

-- Project preset: "auto", "unity", "console", "large", "none"
-- "auto" detects Unity/Godot/Large projects and applies optimized settings
preset = "auto",

-- Logging level: vim.log.levels.WARN, INFO, DEBUG, TRACE
log_level = vim.log.levels.WARN,

-- === Performance & Loading ===
-- Defer project loading until first C# file is opened
deferred_loading = true,
deferred_loading_delay_ms = 500,

-- Diagnostic throttling reduces UI lag during heavy file changes
diagnostic_throttling = {
enabled = true,
debounce_ms = 500, -- Wait 500ms before requesting diagnostics
visible_only = true, -- Only request diagnostics for visible buffers
},

-- === File Watching Configuration ===
-- Directories to completely ignore (exact case-insensitive match)
ignore_dirs = {
"Library", "Temp", "Obj", "Bin", ".git", ".idea", ".vs",
".godot", ".mono", "node_modules"
},

-- Glob patterns to exclude (follows gitignore syntax)
-- Example: { "*.generated.cs", "**/foo/**" }
ignore_patterns = {},

-- List of extensions to watch
watch_extensions = {
".cs", ".csproj", ".sln", ".slnx", ".slnf",
".props", ".targets", ".razor", ".cshtml", ".xaml"
},

-- Parse .sln/.slnx/.slnf to limit watch scope to project folders
-- Highly recommended for performance on monorepos
solution_aware = true,

-- Respect standard .gitignore files
respect_gitignore = true,

-- === Event Processing ===
-- Batch multiple events into single notifications
batching = {
enabled = true,
interval = 300, -- Window to coalesce events (ms)
},

-- Max events to process per chunk (prevent UI freeze)
-- Default: 1000 (Unity), 100 (others)
max_events_per_batch = 1000,

-- Debounce time for processing file system events
processing_debounce_ms = 150,

-- Quiet period (seconds) required after activity before triggering full scans
-- Helps prevents freezing during massive changes (e.g. Unity re-import)
activity_quiet_period = 5,

-- Rename detection window (ms) to merge delete+create into rename
rename_detection_ms = 200,

-- === Advanced / Fallback ===
-- Polling interval (ms) for fallback watcher and resilience checks
poll_interval = 5000,

-- Force non-native polling mode (not recommended unless fs_event fails)
force_polling = false,

-- Enable auto-restore of NuGet packages on .csproj change
enable_autorestore = true,

})
```

### 2. Common Workflows

#### **Working with Unity**
1. Open your Unity project folder in Neovim.
2. The plugin detects `Assets/` and `ProjectSettings/`.
3. It automatically switches to the **Unity Preset**:
* Ignores `Library`, `Temp`, `Logs`.
* Increases debounce time (Unity likes to touch many files at once).
* Adds `UNT` analyzer rules to your LSP configuration.
4. **Tip**: Use `:RoslynEngineInfo` to confirm Unity mode is active.

### 3. Diagnostics

Run `:checkhealth roslyn_filewatch` to verify your setup. The healthcheck reports:

- **Neovim version** compatibility
- **libuv** capabilities (fs_event, fs_poll)
- **Platform** detection and OS-specific notes
- **Rust native module** status (loaded or fallback)
- **External tools**: fd, dotnet, watchman, fswatch
- **Scanning tier** priority chain (which backend is active)
- **Active Roslyn LSP clients** and their root directories

---

## Command Reference

Most commands are **interactive**—if you run them without arguments, a selection menu will appear.

### Core
| Command | Description |
|---------|-------------|
| `:RoslynFilewatch status` | **Debug Tool**: Shows active watcher status, tracked projects, and health. |
| `:RoslynFilewatch reload` | **Recovery**: Forces a full file resync and tells the LSP to reload all projects. |
| `:checkhealth roslyn_filewatch` | Shows comprehensive environment diagnostics. |
|---|---|

## Maintainer Guide

For developers contributing to `roslyn-filewatch.nvim`, this section details the architecture.

### Architecture Overview

The plugin follows a **Unidirectional Data Flow** pattern to maintain synchronization between the File System and Roslyn with minimal latency and memory usage.

```mermaid
flowchart TD
FS[File System] -->|Events| FS_Event[fs_event.lua]
FS_Event -->|Burst Detection| Regen[regen_detector.lua]
Regen -->|Filter/Debounce| Buffer[Event Buffer]
Buffer -->|Chunked Flush| Watcher[watcher.lua]
Watcher -->|Process Queue| Snapshot[snapshot.lua]
Snapshot -->|Diff| Changes[Detected Changes]
Changes -->|Heuristic| Rename[rename.lua]
Rename -->|Batch Notify| Notify[notify.lua]
Notify -->|LSP JSON| Roslyn[Roslyn LSP]
```

#### Scanning Tiers

The plugin uses a priority chain for file scanning, automatically selecting the fastest available:

```
┌─────────────────────────────────┐
│ Tier 1: Rust native module │ ← Fastest (1-3ms, filtered at native level)
│ roslyn_filewatch_rs │
├─────────────────────────────────┤
│ Tier 2: fd / fdfind │ ← Fast (async subprocess, streams results)
│ External process │
├─────────────────────────────────┤
│ Tier 3: Pure Lua scanner │ ← Slowest (chunked via vim.defer_fn, never blocks)
│ uv.fs_scandir + uv.fs_stat │
└─────────────────────────────────┘
```

All tiers are **fully non-blocking** — the plugin never freezes the Neovim UI during scanning.

#### Core Components (V2 Architecture)
* **`watcher.lua`**: The orchestrator. Manages the lifecycle of event handles and the **Self-Healing Watchdog** that restarts frozen listeners. Implements **Chunked Processing** to handle thousands of events.
* **`fs_event.lua`**: Upgraded to use Neovim 0.10+ native API. Implements **Dynamic Debounce** and feeds events into the regeneration detector.
* **`regen_detector.lua`**: Detects massive file operations (like Unity Asset re-imports or `git checkout`), switching the watcher to Low-Overhead Mode to prevent GC pressure.
* **`snapshot.lua`** & **`roslyn_filewatch_rs`**: The source of truth. Uses a heavily optimized pure-Rust backend to pull full repository snapshots natively in 1-3 milliseconds, completely bypassing Lua loop bottlenecks.
* **`sln_parser.lua`**: Uses isolated background libuv OS threads (`vim.uv.new_work`) to parse massive XML solution files completely off the main UI thread.
* **`fs_poll.lua`**: The polling fallback logic implements **Sparse Polling**, lazily checking directory `mtime` stamps before performing expensive recursive diffs, dropping idle CPU usage by 99%.
* **`notify.lua`**: Handles LSP communication with **Global Deduplication**. Merges redundant requests and sequences `dotnet restore` queues to prevent OOM.

### The Watch Cycle

**1. Startup & Initialization**
* **Async Parsing**: On `LspAttach`, the watcher parses solution files asynchronously.
* **Zero-Block Scan**: Performs a background scan to build the initial snapshot. The UI remains fully responsive.

**2. Event Detection & Optimization**
* **Burst Handling**: If 10,000 files change (Unity Regen), `regen_detector` kicks in, disabling expensive per-file tracking and only marking dirty directories.
* **Chunked Processing**: Events are processed in chunks of 200-1000 (configurable) with 10ms yields to the main loop, ensuring Neovim never freezes.

**3. Synchronization**
* **Changes**: Detected changes are sent to Roslyn via `workspace/didChangeWatchedFiles`.
* **Smart Restore**: If a `.csproj` is touched, a restore is queued. If 50 projects change, they are restored one by one.

### Troubleshooting & Debugging

**"The watcher is not watching files"**
1. Turn on debug logs: `setup({ log_level = vim.log.levels.DEBUG })`.
2. Run `:messages`.
3. Check `:RoslynStatus`.
* **Healthy**: "Watcher: Running (fs_event)"
* **Issue**: "Watcher: Polling (Fallback)" -> Might be slower.

**"It freezes when I change branch"**
1. This means the dirty scan is taking too long.
2. Increase `poll_interval` or `processing_debounce_ms`.
3. Ensure `ignore_dirs` includes your build artifacts (`bin`, `obj`).

**"Slow initial scanning"**
1. Run `:checkhealth roslyn_filewatch` — check the **Scanning Tiers** section.
2. Install the Rust native module (fastest) or `fd` (fast fallback).
3. Add large generated/build folders to `ignore_dirs`.

**"NuGet restore not working"**
1. Ensure `dotnet` CLI is installed and in your PATH.
2. Check `enable_autorestore = true` in your config.
3. Run `:checkhealth roslyn_filewatch` to verify dotnet is detected.

---

## Known Limitations

1. **Massive Repositories (10k+ files)**
* **Issue**: Initial scan might cause a brief CPU spike.
* **Workaround**: `deferred_loading = true` is enabled by default to delay parsing until work begins.
* **Optimization**: Ensure your `ignore_dirs` list includes all build/cache folders (e.g., `dist`, `node_modules`).

2. **Network Shares (NFS/SMB)**
* **Issue**: Native file watching (`fs_event`) is notoriously flaky on network drives.
* **Workaround**: The plugin attempts to fall back to polling, but latency will be higher.

3. **Linux/BSD: "ENOSPC" Error**
* **Issue**: You might hit the system limit for file watchers.
* **Fix**: Increase `fs.inotify.max_user_watches` with `sysctl` (Standard Linux limitation).

4. **External Changes (Git/Unity)**
* **Issue**: Mass changes from outside Neovim (like `git checkout` or Unity re-importing assets) triggers thousands of events.
* **Mitigation**: Automatically throttle these bursts using `processing_debounce_ms` and `activity_quiet_period`, so Neovim stays responsive, but Roslyn might take a few seconds to catch up.

5. **csproj-only Projects (Fixed in v0.3.5)**
* **Previously**: Projects without `.sln`/`.slnx` files had issues with new files not being recognized by the LSP.
* **Now Fixed**: The plugin automatically:
* Recursively scans for all `.csproj` files in the project
* Sends `project/open` notifications when new source files are created or opened
* Triggers Roslyn project reload via csproj CHANGE events (same as opening old files)
* Automatically restores dependencies when needed (if `enable_autorestore = true`)
* Debounces notifications to prevent constant restores and reduce lag
* **Note**: For best results with csproj-only projects, ensure `enable_autorestore = true` is set in your config.

---

## License

MIT License.

## Acknowledgements

- Inspired by the pain of using Roslyn in Neovim without file watchers 😅

- This project relies on several excellent open-source tools:

- **Neovim** — Lua APIs (`vim.fs`, `vim.uv`) used for cross-platform file watching.
- **Roslyn** — Microsoft's C# compiler platform and language server.
- **Watchman** — high-performance file watching for large repositories.
- **fswatch** — cross-platform filesystem change monitor.
- **fd** — fast file discovery used for repository scanning.
- **Rust** and the **ignore** crate — powering the native snapshot module for fast indexing.