Repository navigation
Keep Navigate To's items on disk between sessions - #20689
Draft
xperiandri wants to merge 23 commits into
Draft
xperiandri wants to merge 23 commits into
xperiandri wants to merge 23 commits into
Conversation
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>
Contributor
✅ Release notes checked
Warning No PR link found in some release notes, please consider adding it.
|
xperiandri
force-pushed
the
feature/navigate-to-persistent-cache
branch
from
October 2, 2026 16:03
f8bfbaf to
e993e46
Compare
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
force-pushed
the
feature/navigate-to-persistent-cache
branch
from
October 8, 2026 01:11
e993e46 to
5715d60
Compare
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
force-pushed
the
feature/navigate-to-persistent-cache
branch
from
October 10, 2026 22:28
5715d60 to
0c3ebd8
Compare
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>
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>
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
force-pushed
the
feature/navigate-to-persistent-cache
branch
from
October 29, 2026 22:03
0c65afe to
9c04ee7
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Navigate To keeps each F# file's declarations in the solution's
.vsfolder, so a new session reads them back instead of parsing — including the search that runs while the solution loads.Details
#ifare keyed by a checksum of the text; files with#ifalso by the defines.IVsSolutionWorkingFolders):.vs\<solution>\FSharp\v1. An unsaved solution keeps nothing.🤖 Generated with Claude Code