fix(csharp): fold newly created files into the loaded Roslyn project (#1981)

* fix(csharp): fold newly created files into the loaded Roslyn project

Roslyn only learns which files belong to a project from the
solution/open and project/open notifications _open_solution_and_projects
sends once at startup. The generic didChangeWatchedFiles/open-close cycle
that poll_and_notify sends for every backend on file creation does not
make it re-evaluate the project, so a .cs file created after the project
was already indexed stayed a standalone Miscellaneous Files document,
producing phantom diagnostics (e.g. an incorrect "using directive is
unnecessary") instead of the real ones.

Add an overridable SolidLanguageServer.notify_files_created hook, called
by LanguageServerFileChangeNotifier.poll_and_notify before it opens newly
created files. CSharpLanguageServer overrides it to resend the same
solution/project notifications and wait for Roslyn's own reload-complete
log line before the file is opened; every other backend keeps its current
behavior via the no-op default.

Fixes #1961

* fix(test): use get_language_server_manager_or_raise in csharp fold test

project.language_server_manager is typed LanguageServerManager | None, so
calling .iter_language_servers() on it directly failed ty's unresolved-attribute
check. Agent already exposes a non-Optional accessor for the same value.
This commit is contained in:
Amir Fathi authored and GitHub committed 2026-09-23 15:15:49 +02:00
1 parent cf54869af8
commit 714c260e1f
5 files changed
+93 -5

No files matched your search

+44
View File
@@ -3,6 +3,7 @@ End-to-end test for language server file synchronisation as a result of external
Exercises all three ``FileChangeType`` branches against a real pyright backend:
Created (new caller file), Changed (append a second caller), Deleted (remove the caller file).
Also covers a Roslyn-specific Created regression (test_new_csharp_file_is_folded_into_compilation).
"""
import json
@@ -192,3 +193,46 @@ def test_find_symbol_tool_reflects_external_change_to_open_buffer(tmp_path):
of Serena's own edit tools.
"""
SymbolPositionStaleAfterExternalEditTestCase().run(tmp_path)
@pytest.mark.csharp
def test_new_csharp_file_is_folded_into_compilation(tmp_path):
"""
Regression test for oraios/serena#1961: a .cs file created after the project has already
been indexed used to stay a standalone Miscellaneous Files document to Roslyn, since the
generic didChangeWatchedFiles/open-close cycle that poll_and_notify sends for every backend
is not enough to make Roslyn's project system re-evaluate which files belong to the already-
loaded .csproj. The tell is a phantom 'Using directive is unnecessary' (IDE0005) on a using
the new file genuinely needs, since Roslyn cannot resolve the type it imports.
"""
repo_root = tmp_path / "repo"
shutil.copytree(get_repo_path(LanguageServerId.CSHARP), repo_root)
new_file_rel = "UsesPerson.cs"
new_file_abs = repo_root / new_file_rel
with agent_for_project_context(LanguageServerId.CSHARP, str(repo_root)) as agent:
project = agent.get_active_project_or_raise()
ls = next(iter(agent.get_language_server_manager_or_raise().iter_language_servers()))
new_file_abs.write_text(
"using TestProject.Models;\n\n"
"namespace TestProject;\n\n"
"public class UsesPerson\n"
"{\n"
" public Person Get() => new Person();\n"
"}\n",
encoding="utf-8",
)
assert project.ls_sync_file_system_changes() == 1, "expected exactly one Created event"
diagnostics = ls.request_text_document_diagnostics(new_file_rel, min_severity=4)
messages = [d["message"] for d in diagnostics]
assert not any("unnecessary" in m.lower() for m in messages), (
f"new file was analyzed as a standalone document instead of being folded into the "
f"already-loaded project (phantom 'using directive is unnecessary'): {messages}"
)
assert any("Person" in m and "name" in m for m in messages), (
f"expected the real compiler diagnostic for the missing Person(name, age, email) "
f"argument, which only fires once the file is actually part of the compilation: {messages}"
)