Skip to content

Answer the Navigate To search that runs while the solution loads - #20492

Draft
xperiandri wants to merge 133 commits into
dotnet:mainfrom
xperiandri:feature/navigate-to-while-loading
Draft

Answer the Navigate To search that runs while the solution loads#20492
xperiandri wants to merge 133 commits into
dotnet:mainfrom
xperiandri:feature/navigate-to-while-loading

Conversation

@xperiandri

Copy link
Copy Markdown
Contributor

Draft, and blocked on two things. It cannot merge until the Roslyn contract it implements has
flowed into the package this repository builds against — dotnet/roslyn#85213 — and the diff below
is not yet its own, see What you are looking at.

Navigate To runs a search of its own while a solution is still loading, and F# contributes nothing to
it. NavigateToSearcher dispatches that phase through IAdvancedNavigateToSearchService, which the
F# language service does not implement, so every F# project is reported complete and searched not at
all. No full search follows — that is deliberate on the Roslyn side ("Telemetry shows no meaningful
change if we do a full search after this point") — so on a large solution Ctrl+T lists nothing F#
declares until the user searches again.

The search itself was never the problem: it is a parse away, and it needs no type checking. 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 a 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 now carries whether it was approximate, and the accurate path refuses those and reparses. 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 holding 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 its
documents are done. The parses take turns on the throttle every search shares, so a search during load
cannot take the cores away from the load itself.

What you are looking at

The change is one commit, Answer the Navigate-To search that runs while the solution loads. The rest
of the diff belongs to PRs that are still open and that this one is built on:

HasConditionalDirectives is not in any open PR yet. As those merge, this diff shrinks to its own
commit; until then a branch off main cannot carry this change, because it edits a type main does
not have.

Verification

Three tests in NavigateToSearchWhileLoadingTests.fs: the loading search finds a declaration where
the accurate search raises for want of options; a file behind #if FOO read under the wrong defines
returns nothing and does not poison the accurate search that follows once --define:FOO arrives; and
every project is reported complete exactly once whether or not anything was found in it. Removing the
approximate flag fails the first two, so they are not decorative. Full FSharp.Editor.Tests: 7301
passed, 0 failed.

Checked live as well, with a local Roslyn build of dotnet/roslyn#85213 deployed to the experimental
hive alongside this: on a 26-project mixed C#/F# solution, breaking in the F# implementation gives the
stack ProcessOrderedProjectsAsyncNavigateToSearcher.SearchCachedDocumentsAsync → the external
access adapter → here, with the active document and priority documents intact, and Ctrl+T lists F#
declarations immediately while the solution is still loading.

🤖 Generated with Claude Code

xperiandri and others added 30 commits September 5, 2026 00:01
Build, diagnostics, navigation, rename and formatting go through the IDE
that already has the solution open, so the agent reads what the compiler
and the symbol graph know rather than what the files say.

Records the traps that cost a wrong edit otherwise: an unsaved VS buffer
outranks the file on disk, build_* follows the IDE's active configuration
rather than Debug, and project_add_file appends without a position so it
cannot place a Compile item in an order-sensitive F# project.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LLMs are trained predominantly on pre-existing F# code, which skews
toward older language versions and idioms (option over voption,
sprintf over interpolation, tuples over struct tuples, isNull over
match). Codify the constructs this codebase actually favors so
agent-generated code reaches for the current idiom by default.
* Used safe `ModuleOrNamespaceType` property instead of `entity_modul_type`.
* Used safe `TypeContents` property instead of `entity_tycon_tcaug`.
* Optimized `TextViewEventsHandler` memory usage with `voption`
Always return initialized value for `Entity.entity_modul_type` and `Entity.entity_tycon_tcaug`.
…recomputing diagnostics when document/project version is unchanged
* Refactored the diagnostics cache in `FSharpDocumentDiagnosticAnalyzer` to use a new `CachedDiagnosticsEntry` record, storing `TextVersion`, `ProjectVersion`, `FilePath`, `IsRemoveParensEnabled`, and cached `Diagnostics`.
* The cache key remains `(DocumentId * DiagnosticsType)`, but the value is now the new record. Cache lookup now checks all relevant fields for equality, ensuring diagnostics are reused only when context matches.
* Added `evictRemovedDocuments` to remove cache entries for deleted documents.
* Updated logic for "Remove Parentheses" diagnostics to use the cached flag, and updated cache storage to the new structure.
* `FSharpDocumentDiagnosticAnalyzer` now evicts cached diagnostics when documents/projects/solution are removed, using `WorkspaceChanged` events.
* Replaces `evictRemovedDocuments` with targeted evictDocument/evictProject.
* Cache eviction is now event-driven, not on-demand.
* Also injects optional VisualStudioWorkspace and marks analyzer `[Shared]` for MEF.
…1 resumable-code composition error; async invalidation via cancellableTask and stable emitCache for C# PE references
Expanded record field syntax in pattern matches and constructions for maintainability. Rewrote `CompilationOptions` and `otherOptions` logic using array comprehensions for conciseness and readability. Updated `debounceCts` to allow null values explicitly. Replaced some `Array.iter` usages with `for` loops for consistency.
Replace ConcurrentDictionary + WorkspaceChanged eviction with two
ConditionalWeakTable&lt;Document, CachedDiagnosticsEntry&gt; (syntax/semantic),
keyed by Document snapshot identity matching ProjectCache.Projects idiom.

Drop TextVersion/FilePath/GetTextVersionAsync. Keep ProjectVersion for Semantic.

Builds/tests clean, no behavior change.

Co-authored-by: Copilot &lt;223556219+Copilot@users.noreply.github.com&gt;
Encoding.UTF8.GetMaxByteCount(n) returns 3n+3, which crosses
ArrayPool<byte>.Shared's 1 MB limit at 349,525 characters. Past that the
pool cannot serve or reclaim the rental, so it allocates a fresh array
roughly three times the size of the file on every call and drops it on
Return -- worse than the plain GetBytes allocation this replaced.

Over the 402 sources of one FSharp.Compiler.Service compile that is 3
unpooled rentals totalling 4971 KB; sizing the rental with GetByteCount
brings it to 0 and keeps every rental two buckets lower.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ee update

- Key the caches by DocumentId instead of Document, so entries survive
  solution snapshots while the weak table still drops them when a document
  leaves the solution - no eviction pass on the request path.
- Cache validity now covers everything the diagnostics depend on: file path
  (locations embed it, so a rename keeping the DocumentId invalidates), text
  version, project version (parsing options for syntax, dependent version for
  semantics) and the RemoveParens setting.
- Replace the non-atomic Remove + Add with a locked update; ConditionalWeakTable
  has no atomic replace on .NET Framework and the pair races between concurrent
  syntax/semantic passes over the same document.
- Semantic pass reuses the parse results returned by
  GetFSharpParseAndCheckResultsAsync instead of parsing a second time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both tables are private statics that are never handed out, and
ConditionalWeakTable takes its own private lock internally, so locking the
table is safe and gives the syntax and semantic caches independent locks
instead of serialising their writes through one shared object.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ClearAllCaches` disposed every entry by iterating `singleFileCache.Values`
and then called `Clear()`. The agent loop disposes the entries it removes
via `TryRemove`, so the same entry could be disposed twice: the second
`cp.Unadvise` throws, and the message loop has no `try/with` around it, so
the loop died for the rest of the VS session and project options silently
stopped updating.

Make `TryRemove` the single gate for disposal - `ClearAllCaches` now
removes each entry and disposes only what it actually took ownership of.
This also fixes a subscription leak: the old `Clear()` dropped any entry
added between the `Values` snapshot and the `Clear()` without disposing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`IConnectionPoint.Unadvise` throws when called twice with the same cookie,
so a duplicate `Dispose()` on a subscription escalates into an exception on
whatever thread performed it. Guard the cookie with an interlocked flag so
the second and later disposals are no-ops.

Defence in depth on top of the `TryRemove`-gated disposal in
`FSharpProjectOptionsReactor`: ownership still decides who disposes, this
just keeps a future double-dispose from being fatal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot's built-in symbol provider reads symbols off the Roslyn
compilation, which F# projects do not have, so F# declarations never
appeared in the picker shown for "#".

Proffer a brokered service from FSharp.Editor implementing Copilot's
context-provider and mention-queryable contracts. Declarations come from
the NavigateTo parse-tree cache, so the picker answers without waiting
for a project check; that cache moves into a shared
FSharpNavigableItemsCache used by both features.

A picked mention resolves by fully qualified name against the current
solution, so it survives a file moving, and carries the whole
declaration - doc comment included - as its snippet.

FSharpPackage now registers the provider moniker with Copilot after
package load. The override is no longer DEBUG-only, so it calls its base
implementation, which registers the editor factories.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e path

Sequential per-document scanning made "search" and "declarationsOf" as
slow as the slowest single file; run them across documents concurrently
instead, throttled the same way FindReferencesAsync throttles its
per-document typechecks, so a solution-wide scan does not launch a parse
per document all at once.

FSharpNavigableItemsCache's version-stamp entries move to struct tuples
and its null workspace check to a match, matching this repo's allocation
and null-narrowing conventions on a path every keystroke in the mention
picker hits.

CopilotSymbolMapping collapses its wrapping module into a single
qualified top-level module declaration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FSharpPackage's registration attributes in LanguageService.fs (ProvideOptionPage,
ProvideKeyBindingTable, ProvideEditorFactory, ProvideLanguageService, and six
ProvideLanguageEditorOptionPage entries) reference numeric resource IDs — 100, 101,
6000, 6001, 6008-6012, 6014 — that the shell resolves via a ResourceManager("VSPackage",
...) against FSharp.Editor's own assembly. No such resource set was ever embedded there,
so any lookup throws MissingManifestResourceException.

Add VSPackage.resx with those IDs, wired in with ManifestResourceName=VSPackage the same
way FSharp.ProjectSystem.FSharp.fsproj already backs its own VSPackage.resx. Every value
is copied verbatim from the literal fallback string already passed alongside its resource
ID in the attribute that references it.

Verified the rebuilt FSharp.Editor.dll's manifest now lists VSPackage.resources, which is
what the exception reported missing.

(cherry picked from commit 31d8d5c)
Address review feedback on dotnet#20408.

Every one of the ten strings already exists, translated, elsewhere in
vsintegration, so borrow those targets instead of shipping the new
VSPackage.resx English-only: "F# Source File" and "F# Tools" from
FSharp.ProjectSystem.FSharp's VSPackage.resx, the six option-page names
from FSharp.UIResources' Strings.resx, "F# Interactive" from the FSI
command table.

Also record in the resx header why the page-name IDs live here while the
sibling *PageKeywords stay in FSharp.Editor.resx: the shell resolves the
former through SVsResourceManager against the fixed "VSPackage" basename,
the option-page automation resolves the latter itself out of the
assembly's default resource set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 387eb55)
xperiandri and others added 26 commits September 7, 2026 03:58
A project built with DeterministicSourcePaths or an explicit PathMap hands
the IDE a `--pathmap:` option. FCS applies the map when it pickles the ranges
of the in-memory reference other projects check against, so every symbol
imported from such a project names a mapped, relative file that no workspace
document has, and Go To Definition ends in the generated signature instead of
the source. The map is a property of the build output; the IDE now drops it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…imports are cached

FrameworkImportsCache keys the framework imports, and the TcGlobals built
with them, by the framework set alone. A project reusing the entry got a
fresh TcGlobals only when langVersion or realsig differed, and even then took
pathMap from the cached instance. Since TypedTreePickle applies that map to
every range it writes, the in-memory reference data of each project carried
the --pathmap of whichever project filled the cache first, and a project
without a map handed its consumers file names nothing on disk matches.

pathMap now takes part in the decision like langVersion and realsig, and the
new TcGlobals takes it from the project's own TcConfig, in the incremental
builder and the transparent compiler alike.

Fixes dotnet#20474

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ing project

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`TcSequenceExpression` recursed once per `expr; rest` node of a `seq { }`
body through `tcSequenceExprBody`, `tcSequenceExprBodyAsSequenceOrStatement`
and `tryTcSequenceExprBody` with no stack guard on that spine; only the
leaves were guarded, via `TcExpr`. On a 1 MB thread-pool thread a few
hundred (Debug) to a few thousand (Release) implicit-yield elements ran
the thread out of stack a few frames past a leaf.

With the spine guarded, `CheckNoReraise` then walked the right-nested
`Seq.append`/`Seq.delay` tree with `freeInExpr CollectLocals`, whose
`stackGuard` is `None`, and overflowed in `accFreeInExprNonLinearImpl`.

Run `tcSequenceExprBodyAsSequenceOrStatement` under `cenv.stackGuard` and
give `CheckNoReraise` the guarded free-variable options `CheckEscapes`
already uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <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>
The cross-language search ran after the F# one and visited the consumers one
by one, so on a solution where the F# search takes minutes the C# call sites
were the last thing to appear. The search now starts before the F# one and
runs a few consumers at a time, each search building a compilation; the
results are still reported after the F# uses, each file span once, so the
order in the window is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GetProxyAsync<ICopilotRegistrationService> is an exported brokered
service, so calling it from a background package-load task
constructs Copilot's MEF part graph on that thread. Its constructor
does a blocking JoinableTask wait for the main thread; meanwhile the
Git provider asks for the same proxy from the main thread while
building its own services at solution open, and blocks inside MEF's
PartLifecycleTracker waiting for the part the background thread
owns. Neither side can proceed and Visual Studio hangs permanently.

Move the registration out of the background package-load task and
into LoadComponentsInBackgroundAfterSolutionFullyLoadedAsync (run
after the solution is fully loaded, the way Roslyn's AbstractPackage
defers this kind of work), and switch to the main thread before
asking for the proxy so the two requesters serialise instead of
deadlocking.
Diagnostic aid: on a large solution the "#" mention picker stays
empty and nothing in the Debug pane says why. Log each step of
RegisterCopilotContextProviderAsync so a hang or an early return
(no brokered service container, a null proxy) is visible without a
debugger attached.
IFSharpGoToDefinitionService.TryGoToDefinition is a synchronous contract
Roslyn calls on the UI thread, so the main thread has to wait for the
checker. It did so with a bare Task.Wait, which pumps nothing: the VS
watchdog showed "Please wait for an editor command to finish" after two
seconds and auto-cancelled, while the check itself, and the snapshot
version walk under its lazies, kept running on the pool. Pressing F12
again queued another waiter behind the same lazies, and the dialog
stealing focus pushed the main thread into a focus-lost handler that
blocked on the JTF context lock, so tagger work was cancelled and
semantic classification never arrived.

Wait the way NavigateTo in the same file already does, through
JoinableTaskFactory.Run with the threaded-wait dialog, which keeps the
main thread pumping and gives the user a Cancel button. The Roslyn token
and the dialog token are linked so either cancels the check.

The two TaskCompletionSource bridges in CancellableTasks and
RoslynHelpers were created with TaskCreationOptions.None, so TrySetResult
ran every awaiting continuation inline on whichever thread finished the
F# async - the heavy post-check work of a navigation landed on the pool
thread that completed the check. RunContinuationsAsynchronously moves
those continuations to the pool instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Under --optimize+ the outer task inlines the inner builder's Bind into
its own resumable body, and the inner __resumableEntry then reaches
IlxGen as a bare value: FS3401 on every Windows CI job, while Debug
builds compiled the same code. Build the single cancellableTask the way
NavigateTo does and hand it the linked CancellationTokenSource to
dispose, so there is one builder and nothing to leak.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The picker showed no F# declarations on large solutions. Every query walked
every document of every F# project, parsing the ones nobody had opened, and
`whenAllThrottled` queued a task per document on one semaphore, so a thousand
documents meant a thousand tasks waiting to run while Copilot cancelled the
query and took nothing.

A query now visits the documents in three groups, stopping as soon as it holds
as many declarations as it reports: the documents the user has open, the ones
already in the parse cache, and only then the ones that would have to be
parsed, which get a time budget of their own. The new cache lookup reads no
text, so a closed document costs nothing. `forEachThrottled` pulls documents
through a fixed set of workers instead of starting a task per document, and a
batch of search texts visits each document once for all of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things Find All References did on every search: it searched the whole
solution for Find Implementations, which reports no uses at all and threw the
result away; it started a task per reference found, each fetching the
document's text again; and it swallowed the cancellation that Roslyn raises
when the user closes the window or starts another search, so the search ran on.

The callback now takes the uses of a document at once, so its text is read
once and the reports go out in order, and cancellation propagates. The
documents of a project go through a fixed set of workers instead of a task per
document parked on the throttle, and that throttle is now one budget for the
whole editor - the C# and Visual Basic searches take slots from it too - one
core smaller than the machine, so the thread drawing the results keeps one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Going to a definition in an assembly took the main thread first and kept it:
the generated signature was written, the document opened, and then that
document was type checked and every symbol use in it examined, all in
continuations that resume where they were started. The synchronous copy of the
same routine, which Ctrl+click used, waited for the check with
`runSynchronously` and the tooltip links waited on a modal dialog.

Now only opening the document takes the main thread, with the writing before it
and the check after it on the pool, and the one routine serves all three
callers. Rename keeps the text of the documents its search read so that
Visual Studio asking for a span does not read a closed file from disk on the
main thread. Peek and Go To Definition pass their cancellation token on, so
moving the caret ends the search that is no longer wanted. Finally, a symbol
imported from a project with signature files carries the range of its
implementation, and using it saves a check of the file navigated to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Navigating from F# to a C# or Visual Basic declaration enumerated every
declaration the project holds and mapped each one into an F# external symbol
until one matched. That ran on every navigation and on every Ctrl+hover over
such a symbol. The compilation can be asked for the symbol by documentation
comment id, which F# symbols already carry, so the enumeration is now only
what answers when the id cannot be parsed - the operators, mostly.

Two more places did work they did not need: the signature-to-implementation
paths asked the file system whether a file exists before asking the workspace
for it, though a file the workspace does not hold cannot be navigated to
either way; and a failed cross-language lookup checked the whole project once
per target framework, where the parsed declarations of the other frameworks
have already been searched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Searching each file of a multi-targeted project once left two holes, and a
solution that uses conditional compilation widely fell through both: rename and
Find All References reported the same use once per target framework.

A secondary instance re-searched a file whenever it held any conditional
directive, so an inactive `#if DEBUG` was enough to search it again under every
target framework. Only the defines the two instances disagree on can make a
shared file parse differently, so the directives are now read for the idents
they test and the file is re-searched only when one of those defines is in that
difference. Instances whose defines match skip their shared files outright,
which also spares the checker the project builds those searches would force.

The uses themselves were never deduplicated on the F# side, so a file two
projects compile - the instances of one project file, or two project files
sharing a source file - reported its every use twice over. A range carries its
file, so the first project to report one keeps it.

The test project gained a use beside the `#if FOO` block and a file guarded by
a define both instances share: its only conditional use used to sit inside the
disabled branch, which is why the duplicates went unnoticed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grouping the target-framework instances of a project file sorted each group to
read its head, though what follows the head is never ordered: every secondary
instance is searched the same way. One pass for the best-ranked instance says
that outright, and `Array.minBy` keeps the first of equal rank, which is what
the stable sort put at the head.

Sorting the whole solution once and letting the grouping keep that order reads
better still, but it measures worse - 86 instances over 26 project files, on
net472: 14.5 us and 16.1 KB for the pass per group against 17.2 us and 17.4 KB
for the single sort, and 15.0 us and 16.7 KB for the list-and-sort this
replaces. Neither number matters next to the project checks the grouping
schedules; the sort is dropped because it is not paid for.

`start` becomes `startSearching`, with the reason it is not awaited written
down, and the callback that collects the uses becomes a function rather than a
lambda bound to a name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cross-language lookup builds a candidate per symbol shape and the usages
search carries a definition item beside its project all the way to the report,
so both allocate an option and a tuple per element on paths that run over every
declaration of a project. Neither escapes to a public surface: the pairs live
inside one function or one module, which is where the F# guidance puts value
options and struct tuples.

`FindDeclExternalType`, `FindDeclExternalParam` and `ofRoslynSymbol` answer with
value options and struct tuples; `FindUsagesService` carries `struct
(definitionItem, project)` and `struct (definitionItem, location)` through the
cross-language search and the report; the navigable-items grouping takes its
instances as an array. `Extensions` gains the `ValueOption` counterparts the
conversion needs.

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 take turns on the throttle every search shares, so a search
during load cannot take the cores away from the load itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

❗ Release notes required

You can open this PR in browser to add release notes: open in github.dev


✅ 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.100.md No current pull request URL (#20492) found, please consider adding it
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md No current pull request URL (#20492) found, please consider adding it

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