Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions docs/plans/SCRIPTING-FILEBASED-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,18 @@ remaining gaps are explicit rather than implied.
- [x] **`SolutionLoader` returned any existing file as a project target**, so `Program.cs` was handed
to `MSBuildWorkspace.OpenProjectAsync` and file-based mode was unreachable. Explicit file targets
are now gated on `.sln`/`.slnx`/`.csproj` — implements [SCRIPT-DETECT]
- [x] Ambiguous multi-solution discovery returns an **error** rather than a synthetic workspace: the
directory path fails the file-existence guard in `OpenProjectlessAsync` — [SCRIPT-DEGRADE]
- [x] Ambiguous multi-solution discovery returns an **error** rather than a synthetic workspace, and
keeps doing so now that a project-less directory defers instead of failing:
`SolutionLoader.FindAmbiguousSolutions` separates ambiguity from absence, so only the genuinely
empty root takes the deferred path — [SCRIPT-DEGRADE]
- [x] Confirmed `AddCrossLanguageMetadataReferences` still runs on the MSBuild path after the
`OpenCoreAsync` reordering
- [x] Wrap per-file I/O in closure expansion so one unreadable file degrades that file only —
[SCRIPT-DEGRADE]
- [x] Honor `CancellationToken` inside the closure read loop
- [ ] Distinguish "absent" from "ambiguous" in the error *message*; both currently report the
no-solution-found text — [SCRIPT-DEGRADE]
- [x] Distinguish "absent" from "ambiguous" in the error *message*: absent now defers to lazy
per-file loading, and ambiguous reports every candidate solution plus the
`csharp.solution_path` setting that resolves it — [SCRIPT-DEGRADE]

### F# Sidecar — scripts

Expand Down Expand Up @@ -171,8 +174,11 @@ Coarse, real-artifact tests only — real files on disk, real Roslyn, real FCS,
- [x] Shebang produces no diagnostic — [FILEBASED-SHEBANG]
- [x] `.csx`: script semantics load and the script `#load` path resolves — [CSX-OPTIONS]
- [x] Closure cycle (`a.cs` includes `b.cs` includes `a.cs`) terminates — [SCRIPT-CLOSURE]
- [x] A directory with neither project nor root file is an error, not a synthetic workspace —
- [x] A directory with neither project nor root file defers to lazy per-file loading rather than
building a synthetic workspace, and each loose file becomes its own ad-hoc project —
[SCRIPT-DEGRADE]
- [x] A directory holding several solutions is an **error** naming the candidates and
`csharp.solution_path`, never the deferred path — [SCRIPT-DEGRADE]
- [x] `Classify` maps extensions to compilation models — [SCRIPT-DETECT]

`sidecars/SharpLsp.Sidecar.FSharp.Tests/FSharpScriptTests.fs`:
Expand Down
21 changes: 16 additions & 5 deletions docs/specs/SCRIPTING-FILEBASED-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,12 +306,23 @@ yields two closures, not one project containing both.

## 8. Error handling and degradation `[SCRIPT-DEGRADE]`

- A path that resolves to no supported document kind returns a `Result` failure. It must not be
silently converted into an empty synthetic workspace — that turns a real "I could not load your
- A **file** path that resolves to no supported document kind returns a `Result` failure. It must not
be silently converted into an empty synthetic workspace — that turns a real "I could not load your
code" into a wall of phantom diagnostics.
- Ambiguous solution discovery (multiple `.sln` under the root) already returns "no target" from
`SolutionLoader`. That case is **ambiguity, not absence**, and must surface as an error asking the
user to choose. Treating it as file-based mode would silently mis-analyze an entire repository.
- A **directory** holding no solution or project at all is not a failure. The host opens a workspace
folder eagerly, before any document exists, so `OpenCoreAsync` records the root as project-less and
returns success, deferring workspace creation to the first document update. That document is then
loaded as a file-based app or script, and each subsequent loose file is added as its own ad-hoc
project — two independent files in one folder stay two compilations, per [SCRIPT-ANTIPATTERN].
`IsLoaded` stays false until a document arrives, so nothing claims a workspace exists before one
does.
- Ambiguous solution discovery (multiple `.sln` under the root) also returns "no target" from
`SolutionLoader`. That case is **ambiguity, not absence**, and must surface as an error naming every
candidate and the `csharp.solution_path` setting that resolves it — never the project-less deferral
above. Treating it as file-based mode would silently mis-analyze an entire repository: no project
reference resolves, and every cross-project type becomes a phantom "not found" diagnostic.
`SolutionLoader.FindAmbiguousSolutions` is what distinguishes the two, and
[WORKSPACE-SOLUTION-PATH] specifies the setting the message points at.
- Any I/O during closure expansion is wrapped; a failure to read one included file degrades that file
only and is reported as a diagnostic, leaving the rest of the closure loaded.

Expand Down
27 changes: 27 additions & 0 deletions docs/specs/SHARPLSP-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,33 @@ The project system is the hardest engineering problem in .NET tooling. [MSBuild]
- **Multi-targeting:** Projects targeting multiple TFMs (e.g., `net8.0;net48;netstandard2.0`) present multiple analysis contexts. SharpLsp exposes a custom LSP extension for users to select the active TFM, defaulting to the first.
- **Project-less files:** A `.cs` [file-based app](https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps), a `.csx` Roslyn script, and a `.fsx` F# script are all first-class editing targets with no owning project. Their compilation closure is derived from the root file — `#:include` for file-based apps, `#load` for scripts — and never from the containing directory. See [SCRIPTING-FILEBASED-SPEC.md](SCRIPTING-FILEBASED-SPEC.md).

#### Choosing the Solution to Open `[WORKSPACE-SOLUTION-PATH]`

The host sends one path to each sidecar's `workspace/open`. When that path is a
directory, the C# sidecar discovers a target under it: an unambiguous `.sln`,
`.slnx`, or `.csproj` is opened directly. Discovery **never guesses** between
several nested solutions — a monorepo root holding `app/App.sln` and
`other/Other.sln` is ambiguous, and guessing would silently load the wrong half
of the repository.

`csharp.solution_path` in `sharplsp.toml` resolves that ambiguity by naming the
solution to open, absolute or relative to the workspace root:

```toml
[csharp]
solution_path = "app/App.sln"
```

The host resolves the setting and sends the **solution file** rather than the
root, so the sidecar opens it without running discovery at all. The setting
falls back to workspace-root discovery when unset, and when it names a path that
is not an existing file — a stale or misspelled entry degrades to auto-discovery
instead of wedging the workspace on a path that cannot load.

Without this, an ambiguous root loads no solution, and every semantic
request — hover, completion, diagnostics, navigation — returns empty for the
whole workspace.

### 2.6 Binary Layout & Installation

**The `sharplsp` binary is bundled inside every per-platform VSIX.** A user who installs the VS Code extension gets a fully working LSP server with zero additional steps. Extensions are NOT thin clients that require a system-installed binary — the binary ships inside the extension.
Expand Down
20 changes: 20 additions & 0 deletions docs/specs/SOLUTION-EXPLORER-SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ Per-file symbols are sourced by language, never by a single parser:

The F# path reuses the **same** sidecar `documentSymbol` request that powers the editor outline, mapping the nested FCS symbols (module, namespace, type, DU case, member) into the shared `FileSymbol`/`SymbolNode` tree model using each symbol's full range. The F# sidecar must be threaded into `workspace_symbols::handle`; when it is unavailable the project's `.fs` files contribute no symbols rather than failing the whole request.

### Live-Buffer Path Identity [SE-LIVE-BUFFER]

`sharplsp/workspaceSymbols` MUST parse the latest open-buffer text, including
unsaved and rapid successive edits. Disk content is used only when no open VFS
document denotes the source file.

The editor URI and the project model can use different native paths for the same
file. In particular, Windows runners can send an 8.3 path such as
`C:\Users\RUNNER~1\...`, while the sidecar reports the expanded
`C:\Users\runneradmin\...` path. The VFS therefore resolves and caches the
editor path when the document opens, then compares both the original URI path
and that canonical path during native-path lookup. Canonicalizing only the
project-model path is insufficient because it leaves the editor's aliased path
unchanged and incorrectly falls back to stale disk text.

Path comparison also ignores Windows verbatim prefixes and casing differences.
The coarse VS Code explorer tests prove that the tree reflects an unsaved rename
and the final value in a burst of renames; the VFS alias regression test covers
the reverse-alias lookup independently of hosted-runner path spelling.

### Request: `sharplsp/workspaceSymbols`

**Params:**
Expand Down
112 changes: 61 additions & 51 deletions editors/vscode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading