Closes #47926
## Summary
When using a uv (or Poetry/PDM) workspace with multiple subprojects,
Python LSP servers (Pyright, Ruff, ty, etc.) are initialized with the
subproject directory as their workspace folder instead of the workspace
root. This happens because `PyprojectTomlManifestProvider::search()`
returns the first (innermost)
`pyproject.toml` found walking up the directory tree.
For example, in a uv workspace like:
main-project/
├── pyproject.toml # workspace root with [tool.uv.workspace]
├── uv.lock
├── packages/
│ └── project-api/
│ └── pyproject.toml # subpackage
└── projects/
└── project-a/
└── pyproject.toml # subpackage
Opening a file in `packages/project-api/` would register
`packages/project-api/` as the LSP workspace folder instead of
`main-project/`.
## Approach
The fix uses lockfile existence as a heuristic to detect workspace
roots. The updated `search()` method walks all ancestors (similar to
`CargoManifestProvider`) and:
- Tracks the **innermost** `pyproject.toml` as a fallback
- Tracks the **outermost** `pyproject.toml` that has a sibling lockfile
(`uv.lock`, `poetry.lock`, `pdm.lock`, or `Pipfile.lock`)
- Returns the outermost workspace root if found, otherwise falls back to
the innermost
This works within the existing `ManifestDelegate` interface (existence
checks only, no file content reading).
| Scenario | Result |
|---|---|
| uv workspace (root `pyproject.toml` + `uv.lock`) | Returns workspace
root |
| Poetry workspace (root `pyproject.toml` + `poetry.lock`) | Returns
workspace root |
| Simple project (single `pyproject.toml`, no lockfile) | Returns
project dir (unchanged) |
| Independent subprojects (no lockfile at any level) | Returns each
project's own dir (unchanged) |
Since the manifest provider is set at the Python **language** level,
this fix applies to all Python LSP servers (Pyright, Ruff, ty, etc.).
## Test plan
- [x] Added unit tests for `PyprojectTomlManifestProvider` covering all
scenarios above
- [x] Existing integration test
`test_running_multiple_instances_of_a_single_server_in_one_worktree`
passes (independent subprojects without lockfiles)
- [x] `cargo check -p languages` compiles cleanly
- [x] Manual testing with a real uv workspace (Pyright and Ruff both
receive correct workspace root)
Release Notes:
- Fixed Python LSP servers (Pyright, Ruff, etc.) using the wrong
workspace folder in uv/Poetry/PDM workspaces with multiple subprojects.2 files changedLine totals unavailable