Repository navigation
Parse each file of a multi-targeted F# project once in Go To All - #20483
xperiandri wants to merge 16 commits into
Conversation
✅ Release notes checked
|
This comment has been minimized.
This comment has been minimized.
576ffda to
e81a37e
Compare
T-Gro
left a comment
There was a problem hiding this comment.
🤖🕵️ Please shorten the description using this guidance. Focus on the problem and why the change is needed, in simplified technical English. Leave the implementation inventory to the Files tab and retain necessary caveats.
e81a37e to
952d987
Compare
|
🔍 Tooling Safety Check — Affects-Design-Time
|
952d987 to
48fc560
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
One or more issues must be addressed before approval.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
This PR optimizes Go To All for multi-targeted F# projects by caching parses, delaying text reads, and throttling document searches.
Changes:
- Adds define-aware navigable-item caching and deferred source reads.
- Adds multi-target test infrastructure and regression tests.
- Documents the performance improvement in Visual Studio release notes.
| File | Description |
|---|---|
| vsintegration/tests/FSharp.Editor.Tests/MultiTargetNavigateToSearchTests.fs | Updated as part of this pull request. |
| vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs | Updated as part of this pull request. |
| vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj | Updated as part of this pull request. |
| vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs | Updated as part of this pull request. |
| docs/release-notes/.VisualStudio/18.vNext.md | Updated as part of this pull request. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
T-Gro
left a comment
There was a problem hiding this comment.
🤖🕵️ AI review — verify independently.
2dd51b3 to
f20b658
Compare
…t the multi-target tests Brings the fixes pushed to dotnet#20518 and dotnet#20483 onto the integration branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The port of dotnet#20483: parses are kept per file path and define set, a parse with no conditional directives is shared by every target-framework instance, and TryGetCachedNavigableItems takes the document. The mention picker commits already call it that way, so the branch did not build without this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A standalone script has its options kept per document, so its project never produces the defines and language version a parse is keyed by. A parse that holds conditional directives went into the cache under defines such a document never reads back, and a document without a path was not kept at all, so every search of the unchanged document asked for the parse again. Such a document's navigable items are now kept under its DocumentId with the text version. Nothing is shared across projects, and a project whose first parse makes it produce its options is still keyed by them from the next search on. This is 1b15890 of dotnet#20483 on the shared items cache this branch has. A parse without directives was already kept here, under the key every instance reads, so the test also searches a script with an #if. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch still had only that bullet for dotnet#20483. It gives way to the one that describes what the PR does now, as on the PR branch (998ae4a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the multi-target tests Brings the fixes pushed to dotnet#20518 and dotnet#20483 onto the integration branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The port of dotnet#20483: parses are kept per file path and define set, a parse with no conditional directives is shared by every target-framework instance, and TryGetCachedNavigableItems takes the document. The mention picker commits already call it that way, so the branch did not build without this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A standalone script has its options kept per document, so its project never produces the defines and language version a parse is keyed by. A parse that holds conditional directives went into the cache under defines such a document never reads back, and a document without a path was not kept at all, so every search of the unchanged document asked for the parse again. Such a document's navigable items are now kept under its DocumentId with the text version. Nothing is shared across projects, and a project whose first parse makes it produce its options is still keyed by them from the next search on. This is 1b15890 of dotnet#20483 on the shared items cache this branch has. A parse without directives was already kept here, under the key every instance reads, so the test also searches a script with an #if. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch still had only that bullet for dotnet#20483. It gives way to the one that describes what the PR does now, as on the PR branch (998ae4a). 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>
A standalone script has its options kept per document, so its project never produces the defines and language version a parse is keyed by, and every search of the unchanged script asked for the parse again. Such a document's navigable items are now kept under its DocumentId with the text version. Nothing is shared across projects, and a project whose first parse makes it produce its options is still keyed by them from the next search on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cache key is built from the parsing options the document's parse is made with: the ones the options reactor already holds, else the awaited ones. That removes the per-document cache, the cold-project branch, the sentinel define set and the forwarding members for the options peek, and lets the key say in its type whether a parse depends on the defines. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
One search helper for the three that repeated it, the unused synthetic files and excluded-file data gone, the edit test run in both search orders, and the comments that restated the test names replaced by those names. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Documents that AdhocWorkspace.AddSolution loads each carry their own version, so no instance could read another's parse and the tests passed on main. They now share a version per file, as the projects of a loaded solution do, and a test counts the parse requests across the instances. Co-Authored-By: Claude Sonnet 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>
Head branch was pushed to by a user without write access
502ab34 to
064ba58
Compare
…t the multi-target tests Brings the fixes pushed to dotnet#20518 and dotnet#20483 onto the integration branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The port of dotnet#20483: parses are kept per file path and define set, a parse with no conditional directives is shared by every target-framework instance, and TryGetCachedNavigableItems takes the document. The mention picker commits already call it that way, so the branch did not build without this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A standalone script has its options kept per document, so its project never produces the defines and language version a parse is keyed by. A parse that holds conditional directives went into the cache under defines such a document never reads back, and a document without a path was not kept at all, so every search of the unchanged document asked for the parse again. Such a document's navigable items are now kept under its DocumentId with the text version. Nothing is shared across projects, and a project whose first parse makes it produce its options is still keyed by them from the next search on. This is 1b15890 of dotnet#20483 on the shared items cache this branch has. A parse without directives was already kept here, under the key every instance reads, so the test also searches a script with an #if. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch still had only that bullet for dotnet#20483. It gives way to the one that describes what the PR does now, as on the PR branch (998ae4a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


In multi-targeted F# solutions, Go To All (Ctrl+T) parsed every file once per target framework, so a search stalled on the same text being parsed again and again. A file is now parsed once and every target framework reuses the parse, unless conditional compilation makes the frameworks differ, and a project's documents are searched through a throttle instead of all at once.
F# results still appear only after the solution has loaded.
🤖 Generated with Claude Code