A lightweight file-watching plugin for Neovim that keeps the Roslyn LSP in sync with file changes.
⚡ Why?
Roslyn doesn’t watch your project files by default in Neovim. Without this, you often need to :edit! or restart the LSP when adding/removing/modifying files.
This plugin adds a proper file system watcher so Roslyn always stays updated.
-
Watches your project root recursively using Neovim’s built-in
vim.uv -
Detects file create / change / delete using
uv.fs_eventanduv.fs_poll. -
Detects file renames reliably (
didRenameFiles). -
Sends
workspace/didChangeWatchedFilesnotifications to Roslyn -
Configurable:
- Ignore dirs (
bin,obj,.git, etc.) - File extensions to watch (
.cs,.csproj,.sln, …)
- Ignore dirs (
-
Auto cleans up watchers when LSP detaches
-
Batching of events to reduce spam.
-
Watchdog auto-resyncs when events are missed.
-
Closes buffers for deleted files automatically.
-
Works seamlessly in Unity projects with Roslyn.
This plugin does not provide a Roslyn language server on its own.
You must already have an LSP client for Roslyn installed and configured.
You can use one of the following:
- roslyn.nvim — A Neovim plugin that manages Roslyn LSP automatically.
- nvim-lspconfig (roslyn_ls) — Manual configuration for Roslyn LSP via
nvim-lspconfig.
The file watcher integrates with whichever Roslyn LSP client you are using,
and will forward file system events (workspace/didChangeWatchedFiles, workspace/didRenameFiles) to keep Roslyn in sync.
Using lazy.nvim
{
"khoido2003/roslyn-filewatch.nvim",
config = function()
require("roslyn_filewatch").setup({})
end,
},Using packer.nvim
use {
"khoido2003/roslyn-filewatch.nvim",
config = function()
require("roslyn_filewatch").setup()
end,
}require("roslyn_filewatch").setup({
client_names = { "roslyn_ls", "roslyn", "roslyn_lsp" },
ignore_dirs = {
"Library",
"Temp",
"Logs",
"Obj",
"Bin",
".git",
".idea",
".vs",
},
watch_extensions = { ".cs", ".csproj", ".sln", ".props", ".targets" },
batching = {
enabled = true,
interval = 300,
},
poll_interval = 3000, -- fs_poll interval (ms)
poller_restart_threshold = 2, -- restart poller if idle for N seconds
watchdog_idle = 60, -- restart watcher if idle for N seconds
rename_detection_ms = 300, -- window to detect delete+create → rename
processing_debounce_ms = 80, -- debounce high-frequency events
-- Control verbosity of plugin notifications:
-- TRACE < DEBUG < INFO < WARN < ERROR
-- Default: WARN (only warnings & errors are shown)
log_level = vim.log.levels.WARN,
})lua/roslyn_filewatch/
├── watcher.lua # Core orchestrator, starts/stops subsystems per client
├── watcher/
│ ├── fs_event.lua # Low-level uv.fs_event handling
│ ├── fs_poll.lua # Polling fallback for platforms with weak fs_event
│ ├── watchdog.lua # Periodic resync & restart if no events received
│ ├── autocmds.lua # Neovim autocmd integration (BufWrite, BufDelete, etc.)
│ ├── rename.lua # Rename detection (Deleted+Created → didRenameFiles)
│ ├── snapshot.lua # Snapshot tracking of file tree state
│ ├── notify.lua # Thin wrapper for LSP + user notifications
│ └── utils.lua # Path normalization, stat helpers, etc.
This plugin keeps Roslyn aware of file system changes that Neovim or Unity trigger:
-
fs_event (
uv.fs_event)- Listens for low-level file changes.
- Fast and efficient where supported.
-
fs_poll (
uv.fs_poll)- Polls periodically as a fallback.
- Detects missed events and validates file integrity.
-
Snapshots (
snapshot.lua)- Keeps an in-memory map of files and their metadata (mtime, inode, size).
- Allows diffing to detect created, deleted, or changed files.
-
Rename detection (
rename.lua)- If a file is deleted and a new one created within a short window → treat as rename.
- Sends Roslyn
workspace/didRenameFilesinstead of separate delete/create.
-
Batching
- Groups multiple events into a single LSP notification to reduce traffic.
-
Watchdog
- Restarts the watcher if no events are seen for too long (e.g. Unity reload).
- Ensures resilience against dropped events.
-
Autocmds
- Hooks into Neovim’s buffer lifecycle (
BufWritePost,BufDelete, etc.). - Keeps open buffers and file state in sync.
- Hooks into Neovim’s buffer lifecycle (
-
Notifications
- Translates events into Roslyn-compatible LSP notifications:
workspace/didChangeWatchedFilesworkspace/didRenameFiles
- Translates events into Roslyn-compatible LSP notifications:
-
The plugin doesn’t seem to do anything?
- Run
:LspInfoand make sure the active LSP name matches one of the entries inclient_names. - Example: if your LSP shows up as
roslyn_ls, ensureclient_names = { "roslyn_ls" }.
- Run
-
On Linux, file watchers stop working after deleting directories.
- This is a known behavior of
libuv. The plugin automatically reinitializes the watcher when this happens.
- This is a known behavior of
-
Performance concerns on large projects.
- Keep batching enabled (
enabled = true) to reduce spammy notifications. - Tune
intervalfor your workflow (e.g., 200–500 ms for very large solutions).
- Keep batching enabled (
-
On very large repositories (tens of thousands of files):
- Initial snapshot scans can cause short CPU spikes (UI may freeze briefly).
- Memory usage scales with project size (released when projects close).
-
During heavy operations (e.g.
git checkout, Unity regenerating solution files):- Expect a burst of events. With batching enabled, these are grouped safely,
but you may notice slight delays before Roslyn sees all updates.
- Expect a burst of events. With batching enabled, these are grouped safely,
-
These spikes will not crash Neovim, but may temporarily impact responsiveness.
For most Unity/.NET projects, this plugin is good enough and keeps Roslyn in sync without manual restarts.
This project is licensed under the MIT License.
- Inspired by the pain of using Roslyn in Neovim without file watchers 😅
- Thanks to Neovim’s
vim.uvfor making cross-platform file watching possible.