fix(lsp): анализ одиночного файла и untitled-документов без workspace#4240
Conversation
When a client opens a single file (not a folder) VSCode sends `initialize`
with `workspaceFolders`, `rootUri` and `rootPath` all null. Since the
multi-workspace refactor `setConfigurationRoot` bailed out early in that case,
so no `ServerContext` was created; `didOpen` then failed to route the document
("No workspace found") and neither push nor pull diagnostics were produced.
The same happened to `untitled:` buffers even when real workspaces existed,
because their URI never prefix-matches a `file://` workspace root.
Create a default (headless) `ServerContext` at initialization when the client
provides no roots, with a null configuration root so `populateContext` scans
nothing. Track the first registered workspace as the "primary" context and add
`resolveContextForDocument`, which falls back to it for any document that does
not belong to a workspace root (untitled buffers, files opened outside all
roots, single-file mode). Document routing in `BSLTextDocumentService` uses the
new fallback; `getServerContext` keeps its strict containment semantics so
existing callers are unaffected. The synthetic default URI is excluded from
relative-pattern file watchers and never publishes `WorkspaceAddedEvent`, so
path-based watchers are not fed a non-file URI.
Fixes 1c-syntax/vsc-language-1c-bsl#375
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughServerContextProvider now creates a default workspace for no-root initialization, routes untitled documents through a primary workspace fallback, and updates primary selection as workspaces are added or removed. The language server wires in the default workspace at startup, filters watcher roots, and tests cover the new routing behavior. ChangesDefault workspace and document routing
Estimated code review effort: 3 (Moderate) | ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
SonarCloud flags `volatile` on a non-primitive reference (rule S3077), dropping the reliability rating on new code. Replace the volatile URI field with an AtomicReference and use compareAndSet for first-writer-wins, matching the AtomicReference idiom already used elsewhere in the codebase. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/test/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContextProviderTest.java (1)
149-193: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTests assume clean shared-bean state without explicit setup.
testResolveUntitledDocumentRoutesToFirstWorkspace(line 167) doesn't reset state at the start and relies on no stray default context leaking from a previous test using the sharedserverContextProvidersingleton bean. JUnit 5 doesn't guarantee declaration-order execution by default, so iftestRegisterDefaultWorkspace/testResolveUntitledDocumentRoutesToDefaultWorkspacerun without their cleanup executing first (e.g., failed cleanup after an earlier assertion failure), this test could observe an unexpected default context. This is a pre-existing pattern in the file (other tests also rely on manual cleanup rather than isolated@BeforeEachsetup), so it's not new to this PR, but the added tests increase the shared-state surface.Consider adding a
@BeforeEachthat callsserverContextProvider.clear()for defensive isolation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContextProviderTest.java` around lines 149 - 193, The new untitled-document tests in ServerContextProviderTest rely on shared singleton state and can be affected by leftover workspaces or a default context from earlier tests. Add defensive test isolation by clearing serverContextProvider before each test, ideally via a `@BeforeEach` setup in ServerContextProviderTest, so resolveContextForDocument and the related workspace tests always start from a clean state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContextProviderTest.java`:
- Around line 149-193: The new untitled-document tests in
ServerContextProviderTest rely on shared singleton state and can be affected by
leftover workspaces or a default context from earlier tests. Add defensive test
isolation by clearing serverContextProvider before each test, ideally via a
`@BeforeEach` setup in ServerContextProviderTest, so resolveContextForDocument and
the related workspace tests always start from a clean state.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ef34954d-e169-40d2-8e51-2d823d4e80b9
📒 Files selected for processing (5)
src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContextProvider.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServer.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLTextDocumentService.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContextProviderTest.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServerTest.java
The tests share the singleton ServerContextProvider bean and relied on per-test cleanup, which is skipped if an assertion fails mid-test — leaking workspaces (or the default context) into the next test given JUnit 5's unordered execution. Reset the provider before each test for isolation. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
…vate getPrimaryContext has no cross-package caller — it's used only internally by resolveContextForDocument and by the same-package test. Keep the public surface of ServerContextProvider minimal; registerDefaultWorkspace and resolveContextForDocument stay public as they are invoked from the lsp package. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
When initialize arrives without folders/rootUri, a synthetic default workspace is created and marked primary. If a real workspace folder is then added at runtime (workspace/didChangeWorkspaceFolders → addWorkspace), the previous logic kept the empty default context as primary, so untitled buffers and orphan documents kept routing to it instead of the freshly added project folder. Track the primary preferring the first real workspace: the default becomes primary only while no real folder exists and is displaced as soon as one is added; removing the primary repoints to a remaining real workspace, falling back to the default only as a last resort. Tests cover the runtime add/remove scenarios after an empty initialize. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/test/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServerTest.java (1)
162-178: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTest bypasses the real workspace-folder wiring path; overlaps with
ServerContextProviderTest.This test calls
serverContextProvider.addWorkspace(folder)directly rather than driving it through the actualworkspace/didChangeWorkspaceFoldershandler, so it exercises the same routing contract already covered byServerContextProviderTest.testAddingWorkspaceAtRuntimePromotesPrimaryFromDefaultwithout adding coverage for the language-server-level wiring (e.g., watcher registration for the newly added folder) that this test class is meant to verify.Consider driving this scenario through the server's actual workspace-folder-change entry point if one exists, to get genuine end-to-end coverage rather than duplicating the provider-level unit test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServerTest.java` around lines 162 - 178, Use the language-server workspace-folder change path instead of calling ServerContextProvider.addWorkspace directly in addingWorkspaceFolderAfterEmptyInitializeRoutesUntitledToIt; route the scenario through the server’s workspace/didChangeWorkspaceFolders handler (or equivalent entry point) so this test verifies the actual wiring, including any watcher/registration behavior, rather than duplicating ServerContextProviderTest coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServerTest.java`:
- Around line 162-178: Use the language-server workspace-folder change path
instead of calling ServerContextProvider.addWorkspace directly in
addingWorkspaceFolderAfterEmptyInitializeRoutesUntitledToIt; route the scenario
through the server’s workspace/didChangeWorkspaceFolders handler (or equivalent
entry point) so this test verifies the actual wiring, including any
watcher/registration behavior, rather than duplicating ServerContextProviderTest
coverage.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e147b284-71da-4d09-ac7d-81306fa0dbf0
📒 Files selected for processing (3)
src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContextProvider.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContextProviderTest.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServerTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContextProvider.java
The removed test drove the runtime workspace-add scenario via a direct ServerContextProvider.addWorkspace call, duplicating the provider-level unit tests without exercising real language-server wiring (the didChangeWorkspaceFolders handler is fire-and-forget async, so an e2e variant would only add a flaky wait). The runtime add/remove/promote contract stays covered in ServerContextProviderTest, and initialize-without-root default creation is covered by initializeWithoutWorkspaceRegistersDefaultWorkspace. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/test/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServerTest.java (1)
128-141: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winStrengthen the routing assertion to verify identity, not just presence.
The test only asserts
resolveContextForDocument(...)isPresent(), which would also pass if the document happened to resolve to some other context. Given the upstream contract states the default context is created withconfigurationRoot == null, asserting the resolved context is exactly the default one would make this test more precise against regressions inpromoteToPrimary/getPrimaryContext.✅ Suggested stronger assertion
assertThat(serverContextProvider.getAllContexts()) .containsKey(ServerContextProvider.DEFAULT_WORKSPACE_URI); - assertThat(serverContextProvider.resolveContextForDocument(URI.create("untitled:Untitled-1"))) - .isPresent(); + assertThat(serverContextProvider.resolveContextForDocument(URI.create("untitled:Untitled-1"))) + .hasValueSatisfying(ctx -> + assertThat(ctx).isSameAs(serverContextProvider.getAllContexts().get(ServerContextProvider.DEFAULT_WORKSPACE_URI)));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServerTest.java` around lines 128 - 141, The routing assertion in BSLLanguageServerTest is too weak because it only checks that resolveContextForDocument(...) returns a value, not that it resolves to the default context. Update the test around initializeWithoutWorkspaceRegistersDefaultWorkspace to assert the resolved ServerContext is exactly the one stored under ServerContextProvider.DEFAULT_WORKSPACE_URI, so it verifies identity and guards the promoteToPrimary/getPrimaryContext contract rather than just presence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServerTest.java`:
- Around line 128-141: The routing assertion in BSLLanguageServerTest is too
weak because it only checks that resolveContextForDocument(...) returns a value,
not that it resolves to the default context. Update the test around
initializeWithoutWorkspaceRegistersDefaultWorkspace to assert the resolved
ServerContext is exactly the one stored under
ServerContextProvider.DEFAULT_WORKSPACE_URI, so it verifies identity and guards
the promoteToPrimary/getPrimaryContext contract rather than just presence.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 529a4d4f-17cd-4e67-9202-ee960b460b70
📒 Files selected for processing (1)
src/test/java/com/github/_1c_syntax/bsl/languageserver/lsp/BSLLanguageServerTest.java
Tighten initializeWithoutWorkspaceRegistersDefaultWorkspace: assert the untitled document resolves to exactly the context stored under DEFAULT_WORKSPACE_URI rather than merely being present, guarding the promoteToPrimary/getPrimaryContext contract against regressions. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
- repointPrimaryIfRemoved: replace the multi-line updateAndGet block lambda with a plain get/compute/set (S2211 "specify a type for 'current'"); behaviour is unchanged and reads more simply. - ServerContextProviderTest: reword a prose comment that ended in ';' so Sonar no longer flags it as commented-out code (S125). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
The dedup rationale comment left in BSLLanguageServerTest used Class#method tokens and a trailing ';', which Sonar reads as commented-out code. Drop it — the rationale is preserved in the commit that removed the duplicate test. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
…alize After initialize() setConfigurationRoot always registers at least the default workspace (or the real folders), so the primary context is an invariant, not an optional. getPrimaryContext no longer returns an empty Optional when it is unset/missing — it throws IllegalStateException, surfacing misuse (provider used before initialize() or after shutdown()) instead of silently swallowing it. Removed the test that legitimised the empty state. To keep the invariant from breaking under the routing race, removeWorkspace now repoints the primary to a surviving context BEFORE removing the workspace from the map (excluding the one being removed), so primary never transiently points to an already-removed context. Note: the Optional return of getPrimaryContext/resolveContextForDocument is kept to avoid rippling into the five getContextForDocument call sites; making them non-Optional and dropping the now-unreachable "No workspace found" guards is a sensible follow-up. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
- Add a test asserting resolveContextForDocument throws IllegalStateException when no workspace is registered, restoring coverage of the new throw branch (it replaces the deleted "returns empty" test with the loud-fail contract). - Reword the comment in repointPrimaryBeforeRemoval so it no longer ends in ';' with code-like tokens, which Sonar mistook for commented-out code (S125). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
Sonar S5778: the lambda under assertThatThrownBy had two calls that can throw (URI.create and resolveContextForDocument). Move URI.create out so only the call under test can throw inside the lambda. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
Now that getPrimaryContext throws when the primary context is missing, both it and resolveContextForDocument always return a context or fail loudly — the Optional wrapper only ever held a present value. Return ServerContext directly. getContextForDocument is non-null accordingly, so the five now-unreachable "No workspace found" guards (didOpen/didChange/didClose/processDocumentChange/ withFreshDocumentContextInternal) and the NO_WORKSPACE_FOUND_MESSAGE constant are removed; a missing primary now surfaces as an IllegalStateException instead of a silent skip. Tests assert identity (isSameAs) instead of Optional.contains. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_011chMESFUX9VKy4oCuTNcWB
|



Описание
При открытии одиночного файла (а не папки) VS Code присылает в
initializeвсе три «корневых» поля пустыми:workspaceFolders,rootUriиrootPath=null. После перехода на мульти-workspacesetConfigurationRootв этом случае выходил рано и не создавал ни одногоServerContext. В результатеdidOpenне мог смаршрутизировать документ (No workspace found), и диагностики не публиковались ни в push-, ни в pull-модели. То же самое происходило сuntitled:-буферами даже при открытой папке — их URI никогда не совпадает по префиксу сfile://-корнем workspace.Что сделано:
ServerContextProviderregisterDefaultWorkspace()— создаёт дефолтный («безворкспейсный»)ServerContextсconfigurationRoot == null, поэтомуpopulateContext()сразу выходит и ничего не сканирует. Синтетический не-fileURI (DEFAULT_WORKSPACE_URI); метод намеренно не публикуетWorkspaceAddedEvent, чтобы наблюдатели файлов (конфигурации/библиотек) не получали не-файловый URI и не падали наPath.of.primary) контекста — первого зарегистрированного workspace (или дефолтного в режиме одиночного файла); перенацеливается при удалении, сбрасывается вclear().resolveContextForDocument(uri)— индекс → префикс URI workspace → фолбэк на primary для документов вне всех корней (untitled-буферы, файлы вне папок проекта, одиночный файл). СтрогийgetServerContext(uri)оставлен без изменений (семантика «владелец документа»), чтобы не затронуть остальных вызывающих.BSLLanguageServersetConfigurationRoot: при отсутствии folders/rootUri/rootPath заводит дефолтный контекст вместо раннего выхода.buildFileSystemWatchers: относительные наблюдатели строятся только поfile-корням (синтетический URI отфильтрован).BSLTextDocumentServicegetContextForDocument) использует новый фолбэк; pull-diagnosticнаходит документ через индекс уже послеdidOpen.Связанные задачи
Closes 1c-syntax/vsc-language-1c-bsl#375
Чеклист
Общие
gradlew precommit)Дополнительно
Тесты:
ServerContextProviderTest(регистрация дефолтного контекста и идемпотентность, роутинг untitled в дефолтный/в первый workspace, пустой результат без контекстов, отсутствие фолбэка у строгогоgetServerContext) иBSLLanguageServerTest(initializeбез корней заводит дефолтный контекст; untitled при открытой папке уходит в неё, дефолтный контекст не создаётся).gradlew precommit/тесты локально прогнать не удалось — для сборки нужен Gradle 9.6.1 (build-скрипт использует Kotlin multi-dollar interpolation), а в среде разработки его дистрибутив недоступен для скачивания, системный Gradle 8.14.3 этот синтаксис не парсит. Изменения выверены по коду; компиляцию и тесты должен подтвердить CI — прошу обратить на это внимание при ревью.🤖 Generated with Claude Code
Generated by Claude Code
Summary by CodeRabbit
untitled/single-file documents.file-scheme workspace roots.untitledrouting.