FSharp.Editor: full project-options/snapshot invalidation on reference-assembly change instead of point-in-place metadata reference update (like Roslyn)
- Dominant language
- F#
- Stars
- 4.3k
- Forks
- 876
- Avg merge
- 4d 11h
- Merged PRs (30d)
- 131
Description
## Summary
`FSharp.Editor` now watches on-disk `-r:` reference assemblies via `IVsFileChangeEx` (see `FileChangeWatcher.fs`, `FileChangeWatcherHub`, and its consumers in `FSharpProjectOptionsManager.fs` and `WorkspaceExtensions.fs`) and invalidates caches reactively instead of polling `File.GetLastWriteTimeUtc` on every comparison.
However, when a watched reference assembly changes on disk, the current behavior is a **coarse, whole-project invalidation**:
- `FSharpProjectOptionsReactor` (`FSharpProjectOptionsManager.fs`): `cache.TryRemove(projectId)` drops the *entire* cached `(Project * FSharpParsingOptions * FSharpProjectOptions)` tuple for the project, forcing a full recomputation of project options from scratch on next request.
- Transparent-compiler snapshot path (`WorkspaceExtensions.fs`): `latestSnapshots.TryRemove(project.Id)` drops the *entire* cached `FSharpProjectSnapshot`, forcing `FSharpProjectSnapshot.FromOptions` to rebuild the whole snapshot (all file snapshots, not just the changed reference) on next request.
This is architecturally different from how Roslyn handles the equivalent scenario.
## How Roslyn does it
Roslyn's `VisualStudioMetadataReferenceManager` / `FileChangeWatcher` infrastructure (`src/VisualStudio/Core/Def/ProjectSystem/VisualStudioMetadataReferenceManager.cs` and related `FileWatchedReferenceFactory` types) watches individual metadata reference files (PE references) and, on change, invalidates only the cached `PortableExecutableReference`/`MetadataReference` snapshot for *that specific file* (keyed by path in a `ReferenceCountedDisposable` / weak-reference cache). Roslyn's immutable `Solution`/`Project`/`Compilation` snapshots are structured so that a single reference can be swapped:
- `Project.WithMetadataReferences(...)` (and the underlying `Compilation.ReplaceReference` / `Compilation.WithReferences`) only replaces the one `MetadataReference` whose backing file changed; the rest of the `Compilation` (syntax trees, other references, symbol caches for unrelated assemblies) is structurally shared and reused.
- Because `Compilation` is designed around incremental, reference-level substitution as a first-class operation, invalidating one reference does not force Roslyn to re-parse/re-bind the whole project — only re-resolve/re-bind against the one updated reference.
## Why we don't do the same today
`FSharpProjectOptions` and `FSharpProjectSnapshot` are immutable value-like structures without a supported "replace this one reference and keep everything else structurally shared" API. `FSharpChecker`/FCS's incremental checking model is keyed off the whole `FSharpProjectOptions`/`ProjectSnapshot` identity (`OtherOptions` array, `ReferencedProjects`, etc.), not per-reference substitution. Introducing point-in-place reference updates would require:
- A stable, addressable identity for each metadata/reference-assembly entry inside `FSharpProjectOptions`/`ProjectSnapshot` (today they're just `-r:` strings in `OtherOptions`, or `FSharpReferencedProject` values without independent cache keys).
- Incremental-checking support in FCS's `IncrementalBuilder`/`TransparentCompiler` for substituting a single reference without invalidating the whole project's type-checking state (bound signature data, checked file results, etc. that depend on the reference set as a whole).
- Care around cross-project referencing (`AreFSharpInMemoryCrossProjectReferencesEnabled`) where downstream projects' checked state also depends on the changed reference.
This is a change to the core compiler/FCS incremental-checking model, not something that can be done from the editor layer alone (`vsintegration/src/FSharp.Editor`).
## Expected performance win
For solutions with many/large projects referencing a handful of frequently-rebuilt assemblies (e.g. multi-project solutions where a shared library is rebuilt in a loop during active development, or NuGet package caches refreshed on disk), whole-project invalidation means:
- Every keystroke-triggered background check after an external rebuild re-parses and re-type-checks the entire project (all source files), instead of just re-resolving symbols against the one updated assembly.
- For large projects (hundreds of files), this can mean seconds of unnecessary recomputation on every reference-assembly rebuild, compared to Roslyn's near-instantaneous reference swap for C#/VB projects of comparable size.
- This especially affects "F5 workflows" where a referenced class library is rebuilt frequently while iterating on a consuming F# project open in the IDE.
## Proposed follow-up
Track this as a separate, larger effort (likely spanning `FSharp.Compiler.Service`'s `IncrementalBuilder`/`TransparentCompiler` and the `FSharpProjectSnapshot`/`ProjectSnapshot` model) to support per-reference invalidation/substitution, so the editor-layer watcher introduced in `FileChangeWatcher.fs` can eventually trigger a targeted reference swap instead of a full project options/snapshot rebuild.
## Related
- `vsintegration/src/FSharp.Editor/LanguageService/FileChangeWatcher.fs`
- `vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs`
- `vsintegration/src/FSharp.Editor/LanguageService/WorkspaceExtensions.fs`
- `src/Compiler/Service/FSharpProjectSnapshot.fs`
Contributor guide
Research direction
Read FileChangeWatcher.fs, FSharpProjectOptionsManager.fs, WorkspaceExtensions.fs, and src/Compiler/Service/FSharpProjectSnapshot.fs, then trace how a reference-assembly change removes project caches. The work is done when FSharpProjectOptions/FSharpProjectSnapshot and the FCS incremental-checking path support targeted substitution of the changed reference without rebuilding the entire project, including cross-project references.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- fsharp
- Domain
- compilers, devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100