Skip to content

Keep Navigate To's items on disk between sessions - #20689

Draft
xperiandri wants to merge 23 commits into
dotnet:mainfrom
xperiandri:feature/navigate-to-persistent-cache
Draft

xperiandri wants to merge 23 commits into
dotnet:mainfrom
xperiandri:feature/navigate-to-persistent-cache

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Navigate To keeps each F# file's declarations in the solution's .vs folder, so a new session reads them back instead of parsing — including the search that runs while the solution loads.

Details

🤖 Generated with Claude Code

xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 1, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Release notes checked


✅ Found changes and release notes in following paths:

Warning

No PR link found in some release notes, please consider adding it.

Change path Release notes path Description
`src/Compiler` docs/release-notes/.FSharp.Compiler.Service/11.0.200.md No current pull request URL (#20689) found, please consider adding it
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

@xperiandri
xperiandri force-pushed the feature/navigate-to-persistent-cache branch from f8bfbaf to e993e46 Compare October 2, 2026 16:03
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feature/navigate-to-persistent-cache branch from e993e46 to 5715d60 Compare October 8, 2026 01:11
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 8, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feature/navigate-to-persistent-cache branch from 5715d60 to 0c3ebd8 Compare October 10, 2026 22:28
xperiandri added a commit to xperiandri/fsharp that referenced this pull request Oct 10, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri and others added 20 commits October 29, 2026 20:54
With no options from the project system yet, the quick parsing options
carried no source files, and ParseFile throws looking for the last
compiland. Nothing parsed with them before a project had loaded; the
Navigate To search that runs during load does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Navigate To runs a search of its own during load, dispatched through
`IAdvancedNavigateToSearchService`. F# did not implement it, so every F#
project was reported complete and searched not at all, and no full search
follows by design — nothing F# declares could be found until the user searched
again.

The search is a parse away. What it lacked is the project's compilation
options, which do not exist yet during load, so `GetFSharpParseResultsAsync`
raises. `GetFSharpQuickParseResultsAsync` parses with whatever parsing options
the project system has already produced, or defaults: a dictionary read, no
reactor, no I/O, which is what makes it safe to call for every document of
every project while the solution loads.

Those defines can be the wrong ones, and the document's version does not change
when the real options arrive, so the version stamp alone would let an
approximate parse answer the accurate search: a declaration behind `#if` could
be missed, or reported from a branch that never compiles. The cache entry
carries whether it was approximate, and the accurate path refuses those,
reparsing instead. The loading path takes either, since its contract allows
out-of-date results.

`SearchCachedDocumentsAsync` follows the shape of the C# and VB service:
priority documents and the projects that hold them are searched first, results
are reported per document rather than per project so the first ones appear
while the rest are still parsing, and each project is reported complete once it
is done. The parses of every project take turns on one throttle that leaves a
core free, so a search during load cannot take the cores away from the load
itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`main` opened 11.0.200 for SDK 11.0.200; 11.0.100 has shipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`NavigateTo.GetNavigableItems` hands back items whose `Container` can be read
but not constructed: the signature seals the type and hides the `File` and
`Container` cases. That is enough to display a result and not enough to carry
one across a process boundary, which is what caching navigable items on disk
needs — the reader has to rebuild the container it deserialized.

Publish both cases. While the shape is still private, replace `Container`'s
three-element tuple with a named record so the parts have names at the point of
use, and make it a struct: a struct record is laid out inside the case exactly
as the tuple was, so this costs nothing. Building a file plus three nested
containers a million times allocates 144 bytes per chain either way, where a
reference record would take 216.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Linking the note added a copy of it instead of editing the first, and `main` has since
opened 11.0.200 for SDK 11.0.200, leaving 11.0.100 shipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A struct record payload is boxed by the union's structural comparison; three case fields are not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r tests

Test helpers so far put every synthetic file into one Roslyn project. CreateMultiProjectSolution
creates one project per synthetic project with project references, the way VS wires
project-to-project references; CreateMultiTargetSolution creates one project per target
instance sharing the project path and the document paths, the way VS loads a multi-targeted
project.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ead its text only for matches

Roslyn's NavigateTo searcher hands the F# service every target-framework
instance of a project, one after another, and the service parsed every file of
each instance, read the text of every file before matching anything, and
started all of that for a project at once. On a solution with 135 project
instances that meant one parse and one file read per file per framework,
thousands of concurrent tasks, and results that arrived long after the user
stopped typing.

The first instance of a project file in the solution now searches every file;
the other instances only search the files they alone compile and the files
whose parse depends on the defines, known from whichever instance parsed the
file first. A file's text is read only when one of its declarations matched,
and a project's files are searched at most ProcessorCount at a time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Skipping a file in every instance but the first is wrong twice over, as the
review showed. Go To All scoped to the current project submits only that
project, so the instance that was told to stay quiet is the only one asked, and
a shared declaration disappears from it once the parse has taught the service
the file holds no directives. And the flag that decision reads was keyed by path
alone, so an edit that puts a declaration behind `#if` left the previous answer
in place.

Nothing needs skipping. `NavigateToSearcher` pools its seen set with
`NavigateToSearchResultComparer`, which already collapses results by file path
and span, so every instance can report what it compiles.

What the instances should share is the parse, which is what costs. A parse whose
tree holds no conditional directives does not depend on the defines, so it is
kept under a key no define set can equal and every instance reuses it; one that
does hold them is kept per define set, since those instances genuinely parse the
file differently. The entry carries the text version, so an edit is a new entry
rather than a stale flag.

The test that asserted a declaration is reported once across the instances went
with the skip: that is the searcher's job, not this service's. In its place are
the two cases from the review — a lone instance reporting a shared declaration,
and a declaration an edit puts behind a directive — and one that keeps the
define-keyed parse honest by checking such a declaration does not reach the
instance without the define.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cache was a dictionary from a struct tuple of two strings to an anonymous
struct record, spelled out in the type arguments at the one place it is
declared, and read back as `struct (key, path)` wherever it is used. Nothing at
those sites said which string was the defines and which the path.

Give both a name at the top of the file. They stay struct records, so the key
keeps the same equality and the entries allocate no more than before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The defines a file was parsed with are not the whole of what its tree depends on: the
language version decides what the parser accepts, and the defines were taken from the
editing defaults when the project had not produced its options yet - a set another
project really has, so its parse answered for a project that never asked for it, with
the branch of a `#if` that project chose.

The key now carries the language version, and both it and the defines are read only
where the project has produced them: a project without options parses for itself
instead of reading an entry that is not about it. The rest of the parsing options
cannot differ for one path - the editor never applies line directives, and whether a
file is interactive follows from its extension.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The multi-project and multi-target helpers now match the ones dotnet#20462 adds,
struct results included, so this branch rebases onto it without reconciling
two copies. A struct tuple cannot be bound by a `let private` pattern, so the
module-level fixture bindings lose the modifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A struct tuple cannot be bound with `let private` (FS0010: the grammar takes an
access modifier only before a name), so the binding sits in a private AutoOpen
module: the tests here see both names, nothing outside does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Navigate To parses every F# file of a solution the first time it searches it in a session, and while the solution
loads it can only parse with guessed options. FSharpNavigableItemsCache now looks in storage before it parses and
writes after every parse made with the project's options: under a checksum of the text when the tree holds no
conditional directives, and of the text and the defines when it does - the two keys the memory cache already keeps
a parse under. A file without directives is answered before its project has options at all.

The store is one file per document in the folder Visual Studio keeps the solution's state in (.vs\<solution>),
which IVsSolutionWorkingFolders names. A solution with no such folder keeps nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri and others added 3 commits October 29, 2026 20:54
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaying the branch onto main left some files short of the branch's own content; this restores it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feature/navigate-to-persistent-cache branch from 0c65afe to 9c04ee7 Compare October 29, 2026 22:03

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

1 participant