Skip to content

Commit 83ddafb

Browse files
committed
bugfix(roslyn/angular): Fix lsp setup
1 parent 6a11fd7 commit 83ddafb

5 files changed

Lines changed: 276 additions & 22 deletions

File tree

config/languages/lsp.nix

Lines changed: 235 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,33 @@
55
mkDashDefault,
66
...
77
}: let
8+
toolchain = import ../../lib/toolchain.nix {inherit lib pkgs;};
89
pinnedTsserverPath = "${pkgs.typescript}/lib/node_modules/typescript/lib/tsserver.js";
10+
angularLanguageServiceRoot = "${pkgs.angular-language-server}/lib";
11+
angularLanguageServerCommand =
12+
if config'.toolchain.preferProjectTools or false
13+
then toolchain.bin "ngserver"
14+
else "${pkgs.angular-language-server}/bin/ngserver";
15+
typescriptRoot = "${pkgs.typescript}/lib";
16+
typescriptToolsWithAngular = pkgs.vimPlugins.typescript-tools-nvim.overrideAttrs (old: {
17+
postPatch =
18+
(old.postPatch or "")
19+
+ ''
20+
substituteInPlace lua/typescript-tools/process.lua \
21+
--replace-fail ' local plugins_path = tsserver_provider:get_plugins_path()' ' local plugins_path = tsserver_provider:get_plugins_path()
22+
local plugin_probe_locations = {}
23+
if tsserver_provider.npm_local_path and tsserver_provider.npm_local_path:exists() then
24+
table.insert(plugin_probe_locations, tsserver_provider.npm_local_path:absolute())
25+
end
26+
table.insert(plugin_probe_locations, "${angularLanguageServiceRoot}")' \
27+
--replace-fail ' if plugins_path and #plugin_config.tsserver_plugins > 0 then' ' if plugins_path then
28+
table.insert(plugin_probe_locations, plugins_path:absolute())
29+
end
30+
31+
if #plugin_config.tsserver_plugins > 0 then' \
32+
--replace-fail ' table.insert(self.args, plugins_path:absolute())' ' table.insert(self.args, table.concat(plugin_probe_locations, ","))'
33+
'';
34+
});
935
projectTsserverPath =
1036
lib.generators.mkLuaInline
1137
/*
@@ -40,7 +66,7 @@ in {
4066
package = ng-nvim;
4167
};
4268
"typescript-tools.nvim" = mkDashDefault {
43-
package = typescript-tools-nvim;
69+
package = typescriptToolsWithAngular;
4470
setupModule = "typescript-tools";
4571
event = [
4672
{
@@ -49,16 +75,42 @@ in {
4975
}
5076
{
5177
event = "FileType";
52-
pattern = "html";
78+
pattern = "typescriptreact";
5379
}
5480
{
5581
event = "FileType";
56-
pattern = "htmlangular";
82+
pattern = "javascript";
83+
}
84+
{
85+
event = "FileType";
86+
pattern = "javascriptreact";
5787
}
5888
];
5989
setupOpts = {
90+
on_attach =
91+
lib.generators.mkLuaInline
92+
/*
93+
lua
94+
*/
95+
''
96+
function(client, bufnr)
97+
local bufname = vim.api.nvim_buf_get_name(bufnr)
98+
if bufname == nil or bufname == "" then
99+
return
100+
end
101+
102+
local angular_root = vim.fs.find({ "angular.json", "nx.json" }, {
103+
path = vim.fs.dirname(bufname),
104+
upward = true,
105+
})[1]
106+
107+
if angular_root ~= nil then
108+
client.server_capabilities.referencesProvider = false
109+
end
110+
end
111+
'';
60112
settings = {
61-
seperate_diagnostic_server = true;
113+
separate_diagnostic_server = true;
62114
expose_as_code_action = [
63115
"fix_all"
64116
"add_missing_imports"
@@ -92,15 +144,14 @@ in {
92144
setupOpts = {
93145
# Search parent directories for solution files, not just cwd.
94146
# Fixes "LSP not attaching" when the .sln is above the opened file.
95-
broad_search = true;
147+
broad_search = config'.lsp.special.roslyn.broadSearch;
96148

97149
# Once a solution is selected, lock it so re-opening files doesn't
98150
# trigger re-selection or re-initialization (avoids needing restarts).
99-
lock_target = true;
151+
lock_target = config'.lsp.special.roslyn.lockTarget;
100152

101-
# Let roslyn own file watching instead of neovim competing with it.
102-
# Switch to "off" if you notice performance issues.
103-
filewatching = "roslyn";
153+
# Roslyn's built-in watcher can consume huge inotify counts on large repos.
154+
filewatching = config'.lsp.special.roslyn.filewatching;
104155
};
105156
};
106157
};
@@ -122,27 +173,107 @@ in {
122173
cmd = ["${pkgs.vscode-langservers-extracted}/bin/vscode-json-language-server" "--stdio"];
123174
filetypes = ["json" "jsonc"];
124175
};
125-
angular = mkDashDefault {
126-
enable = true;
127-
cmd = ["${pkgs.angular-language-server}/bin/ngserver" "--stdio"];
128-
filetypes = ["htmlangular" "typescript" "html"];
129-
root_markers = [".git" "package.json"];
130-
on_attach =
131-
lib.generators.mkLuaInline
176+
angular = {
177+
enable = mkDashDefault true;
178+
cmd = lib.mkOverride 80 (lib.generators.mkLuaInline
179+
/*
180+
lua
181+
*/
182+
''
183+
function(dispatchers, config)
184+
local root_dir = (config and config.root_dir) or vim.fn.getcwd()
185+
local probe_locations = {}
186+
local seen = {}
187+
188+
local function add_probe(path)
189+
if path ~= nil and path ~= "" and not seen[path] and vim.uv.fs_stat(path) then
190+
seen[path] = true
191+
table.insert(probe_locations, path)
192+
end
193+
end
194+
195+
local project_node_modules = vim.fs.find("node_modules", {
196+
path = root_dir,
197+
upward = true,
198+
type = "directory",
199+
})[1]
200+
201+
add_probe(project_node_modules)
202+
add_probe("${angularLanguageServiceRoot}")
203+
add_probe("${typescriptRoot}")
204+
205+
local function angular_core_version()
206+
local package_json = vim.fs.find("package.json", {
207+
path = root_dir,
208+
upward = true,
209+
type = "file",
210+
})[1]
211+
212+
if package_json == nil then
213+
return ""
214+
end
215+
216+
local ok, content = pcall(vim.fn.readfile, package_json)
217+
if not ok then
218+
return ""
219+
end
220+
221+
local parsed_ok, package = pcall(vim.json.decode, table.concat(content, "\n"))
222+
if not parsed_ok or type(package) ~= "table" then
223+
return ""
224+
end
225+
226+
local version = (package.dependencies or {})["@angular/core"] or (package.devDependencies or {})["@angular/core"] or ""
227+
return version:match("%d+%.%d+%.%d+") or ""
228+
end
229+
230+
return vim.lsp.rpc.start({
231+
"${angularLanguageServerCommand}",
232+
"--stdio",
233+
"--tsProbeLocations",
234+
table.concat(probe_locations, ","),
235+
"--ngProbeLocations",
236+
table.concat(probe_locations, ","),
237+
"--angularCoreVersion",
238+
angular_core_version(),
239+
}, dispatchers)
240+
end
241+
'');
242+
filetypes = mkDashDefault ["htmlangular" "typescript" "typescriptreact"];
243+
root_markers = mkDashDefault ["angular.json" "nx.json"];
244+
on_attach = mkDashDefault (lib.generators.mkLuaInline
132245
/*
133246
lua
134247
*/
135248
''
136249
function(client, bufnr)
137-
-- This shit is the most annoying thing ever
138-
client.server_capabilities.insertReplaceSupport = false
139-
client.server_capabilities.renameProvider = false
140-
client.server_capabilities.referencesProvider = false
141250
client.server_capabilities.documentFormattingProvider = false
142251
client.server_capabilities.documentRangeFormattingProvider = false
143252
client.server_capabilities.documentOnTypeFormattingProvider = false
253+
254+
local ft = vim.bo[bufnr].filetype
255+
if ft == "typescript" or ft == "typescriptreact" then
256+
client.server_capabilities.callHierarchyProvider = false
257+
client.server_capabilities.codeActionProvider = false
258+
client.server_capabilities.completionProvider = false
259+
client.server_capabilities.declarationProvider = false
260+
client.server_capabilities.definitionProvider = false
261+
client.server_capabilities.diagnosticProvider = false
262+
client.server_capabilities.documentHighlightProvider = false
263+
client.server_capabilities.documentLinkProvider = false
264+
client.server_capabilities.documentSymbolProvider = false
265+
client.server_capabilities.hoverProvider = false
266+
client.server_capabilities.implementationProvider = false
267+
client.server_capabilities.inlayHintProvider = false
268+
client.server_capabilities.renameProvider = false
269+
client.server_capabilities.selectionRangeProvider = false
270+
client.server_capabilities.semanticTokensProvider = false
271+
client.server_capabilities.signatureHelpProvider = false
272+
client.server_capabilities.typeDefinitionProvider = false
273+
client.handlers["textDocument/publishDiagnostics"] = function() end
274+
end
144275
end
145-
'';
276+
'');
146277
};
147278
# csharp = mkDashDefault {
148279
# enable = true;
@@ -189,5 +320,88 @@ in {
189320
languages =
190321
config'.lsp.lspServers
191322
// config'.lsp.additionalConfig;
323+
luaConfigRC.dashvim-roslyn-file-change-notifications = lib.nvim.dag.entryAfter ["lsp-servers"] ''
324+
local dashvim_roslyn_group = vim.api.nvim_create_augroup("DashVimRoslynFileChanges", { clear = true })
325+
local dashvim_roslyn_patterns = { "*.cs", "*.csproj", "*.sln", "*.slnx", "*.slnf", "*.props", "*.targets" }
326+
local dashvim_roslyn_extensions = {
327+
cs = true,
328+
csproj = true,
329+
sln = true,
330+
slnx = true,
331+
slnf = true,
332+
props = true,
333+
targets = true,
334+
}
335+
336+
local function dashvim_roslyn_is_project_file(path)
337+
return dashvim_roslyn_extensions[vim.fn.fnamemodify(path, ":e")] == true
338+
end
339+
340+
local function dashvim_roslyn_path_in_root(path, root)
341+
if root == nil or root == "" then
342+
return true
343+
end
344+
345+
local normalized_path = vim.fs.normalize(path)
346+
local normalized_root = vim.fs.normalize(root)
347+
return normalized_root == "/" or normalized_path == normalized_root or vim.startswith(normalized_path, normalized_root .. "/")
348+
end
349+
350+
local function dashvim_roslyn_notify_file_change(path, change_type)
351+
for _, client in ipairs(vim.lsp.get_clients({ name = "roslyn" })) do
352+
if dashvim_roslyn_path_in_root(path, client.config.root_dir) then
353+
client:notify("workspace/didChangeWatchedFiles", {
354+
changes = {
355+
{
356+
uri = vim.uri_from_fname(path),
357+
type = change_type,
358+
},
359+
},
360+
})
361+
end
362+
end
363+
end
364+
365+
vim.api.nvim_create_autocmd("BufWritePre", {
366+
group = dashvim_roslyn_group,
367+
pattern = dashvim_roslyn_patterns,
368+
callback = function(args)
369+
local path = vim.api.nvim_buf_get_name(args.buf)
370+
vim.b[args.buf].dashvim_roslyn_file_existed = path ~= "" and vim.uv.fs_stat(path) ~= nil
371+
end,
372+
})
373+
374+
vim.api.nvim_create_autocmd("BufWritePost", {
375+
group = dashvim_roslyn_group,
376+
pattern = dashvim_roslyn_patterns,
377+
callback = function(args)
378+
local path = vim.api.nvim_buf_get_name(args.buf)
379+
if path == "" then
380+
return
381+
end
382+
383+
local existed = vim.b[args.buf].dashvim_roslyn_file_existed
384+
vim.b[args.buf].dashvim_roslyn_file_existed = true
385+
dashvim_roslyn_notify_file_change(path, existed == false and 1 or 2)
386+
end,
387+
})
388+
389+
vim.api.nvim_create_autocmd("LspAttach", {
390+
group = dashvim_roslyn_group,
391+
callback = function(args)
392+
local client = vim.lsp.get_client_by_id(args.data.client_id)
393+
if client == nil or client.name ~= "roslyn" then
394+
return
395+
end
396+
397+
local path = vim.api.nvim_buf_get_name(args.buf)
398+
if path == "" or not dashvim_roslyn_is_project_file(path) then
399+
return
400+
end
401+
402+
dashvim_roslyn_notify_file_change(path, 2)
403+
end,
404+
})
405+
'';
192406
};
193407
}

docs/ARCHITECTURE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ DashVim is a Nix flake that builds and distributes a Neovim configuration based
99
- `lib/default.nix` calls `inputs.nvf.lib.neovimConfiguration` and passes DashVim options into the `config/` module tree through `extraSpecialArgs`.
1010
- `config/default.nix` imports the Neovim configuration modules for base settings, theme, keybinds, editor features, language support, custom config, and user-provided `additionalConfig`.
1111
- `config/languages/toolchain.nix` injects project-aware command resolvers for known DashVim/nvf LSPs, formatters, and linters when `programs.dashvim.toolchain.preferProjectTools` is enabled; `config/languages/lsp.nix` handles the TypeScript server path special case inside the owning plugin definition.
12+
- `config/languages/lsp.nix` also owns plugin-backed LSP setup such as `typescript-tools.nvim` and `roslyn.nvim`; Roslyn file watching, targeted file-change notifications, broad solution search, and target locking are controlled through `programs.dashvim.lsp.special.roslyn.*`.
1213
- `lib/env.nix` creates the runnable environment by combining the generated Neovim package, optional wrapped opencode package, and shared CLI/runtime dependencies.
1314
- `lib/opencode-config.nix` generates opencode theme, TUI config, opencode config JSON, and bundled skills/commands for both wrapped opencode and Home Manager deployments.
1415
- `lib/toolchain.nix` builds a Rust resolver and per-tool launch symlinks that prefer a different executable from the active Neovim `PATH` and fall back to DashVim's pinned Nix executable.
@@ -21,7 +22,8 @@ DashVim is a Nix flake that builds and distributes a Neovim configuration based
2122
- Users set `programs.dashvim.*` options through the exported module.
2223
- Module options flow into `lib/default.nix` as `config'` and become inputs to the `config/` nvf module tree.
2324
- The nvf output creates the Neovim package used by `lib/env.nix` and `lib/mkPkg.nix`.
24-
- Toolchain preferences flow from `programs.dashvim.toolchain.preferProjectTools` into `config/languages/toolchain.nix`, which overrides active LSP commands and known formatter/linter commands with resolver scripts at Nix module priority 90. TypeScript's `tsserver_path` is resolved in `config/languages/lsp.nix` so the lazy plugin package metadata remains intact.
25+
- Toolchain preferences flow from `programs.dashvim.toolchain.preferProjectTools` into `config/languages/toolchain.nix`, which overrides active LSP commands and known formatter/linter commands with resolver scripts at Nix module priority 90. TypeScript's `tsserver_path` is resolved in `config/languages/lsp.nix` so the lazy plugin package metadata remains intact. Angular TypeScript support is loaded as the `@angular/language-service` tsserver plugin through `typescript-tools.nvim`; standalone Angular LS is reserved for Angular template buffers.
26+
- Roslyn preferences flow from `programs.dashvim.lsp.special.roslyn` into `roslyn.nvim` setup options. The default keeps recursive file watching `"off"` so Roslyn and Neovim do not create broad watchers, and DashVim sends targeted `workspace/didChangeWatchedFiles` notifications when C# project files are saved. Users can opt into `"auto"` or `"roslyn"` only when full external file watching is more important than watcher safety.
2527
- Shared dependencies are collected in `lib/dependencies.nix` and reused by flake packages and Home Manager integration.
2628
- Opencode theme generation uses the configured Base16 colorscheme and optional accent color, then writes generated JSON files through `lib/opencode-config.nix`.
2729

@@ -33,10 +35,12 @@ DashVim is a Nix flake that builds and distributes a Neovim configuration based
3335
- Base16 color contract: UI themes and opencode colors derive from a Base16-compatible palette, with `accentColor` overriding `base0D` when set.
3436
- Global agent instructions: `AGENTS.md` is included in root opencode config, the wrapped opencode instruction paths, and Home Manager deployed opencode instructions. It requires architecture, UI, code guidelines, technical debt, and testing docs to stay current.
3537
- Project tool preference is enabled by default: known LSPs, formatters, and linters first look for a project/shell executable that differs from DashVim's pinned fallback, preserving reproducibility when a project does not provide a tool.
38+
- Angular language ownership is split to avoid duplicate TypeScript LSP results: `typescript-tools.nvim` owns JavaScript/TypeScript buffers and loads Angular's tsserver plugin, while standalone `ngserver` owns `htmlangular` buffers and TypeScript references in Angular projects so references include external templates.
3639

3740
## Tradeoffs
3841

3942
- Generated opencode configs are reproducible, but users must rebuild or redeploy after changing generated inputs.
4043
- `programs.dashvim.opencode.config` can override generated opencode config keys, which is flexible but can replace defaults like `instructions` if users set the same key.
4144
- The module surface is broad and convenient, but changes to defaults can affect many language/editor features at once.
4245
- Project-aware tool resolution depends on Neovim seeing the project shell `PATH` before tools start. LSPs may need restart after entering a shell late.
46+
- Recursive Roslyn file watching may consume significant inotify resources or watch too high a directory such as a home folder. DashVim avoids that by default, but files created outside Neovim may still require opening/saving the file or restarting the LSP before Roslyn refreshes project state.

docs/CODE_GUIDELINES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- Keep Docker/container support Nix-generated unless a Dockerfile already exists or explicit instructions say not to use Nix.
1717
- Use `alejandra` for Nix formatting, available as the flake `format` package.
1818
- Keep project tool resolver runtime logic in tracked Rust source under `lib/toolchain/`; Nix should wire package metadata and generated launchers, not embed large resolver programs in strings.
19+
- Keep plugin-backed LSP behavior with large runtime tradeoffs configurable under `programs.dashvim.lsp.special.*` instead of hard-coding environment-specific defaults.
1920

2021
## Testing And Verification
2122

docs/TECHNICAL_DEBT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This file tracks known issues, limitations, deferred work, and cleanup items tha
1010
- The `programs.dashvim` option surface is broad. Default changes can affect many editor, language, formatter, and opencode behaviors at once.
1111
- Automated testing is currently Nix-focused. No separate unit/integration test suite is documented for Lua plugin behavior or interactive Neovim workflows.
1212
- `programs.dashvim.toolchain.preferProjectTools` covers known DashVim/nvf LSP, formatter, and linter names. Custom direct `vim.lsp.servers`, conform formatter, or nvim-lint linter names still need explicit command configuration until a public extension map exists.
13+
- `programs.dashvim.lsp.special.roslyn.filewatching` defaults to `"off"` to prevent Roslyn or Neovim from recursively watching too much of the filesystem. DashVim sends targeted `workspace/didChangeWatchedFiles` notifications for saved C# project files, but files created outside Neovim may still require opening/saving the file or restarting the LSP before Roslyn refreshes project state.
14+
- `typescript-tools.nvim` does not expose a first-class plugin probe path option, so DashVim patches its generated process arguments to make Nix-provided `@angular/language-service` discoverable by tsserver. Revisit this if upstream adds `tsserver_plugin_probe_locations` or equivalent.
1315

1416
## Maintenance Rules
1517

0 commit comments

Comments
 (0)