Answer the Navigate To search that runs while the solution loads - #20492
Draft
xperiandri wants to merge 133 commits into
Draft
Answer the Navigate To search that runs while the solution loads#20492xperiandri wants to merge 133 commits into
xperiandri wants to merge 133 commits into
Conversation
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<Document, CachedDiagnosticsEntry> (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 <223556219+Copilot@users.noreply.github.com>
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)
(cherry picked from commit 462c68e)
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)
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>
Contributor
❗ Release notes requiredYou can open this PR in browser to add release notes: open in github.dev
Warning No PR link found in some release notes, please consider adding it.
|
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 runs a search of its own while a solution is still loading, and F# contributes nothing to
it.
NavigateToSearcherdispatches that phase throughIAdvancedNavigateToSearchService, which theF# 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
GetFSharpParseResultsAsyncraises.GetFSharpQuickParseResultsAsyncparses with whatever parsingoptions 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
#ifcould be missed, or reported from a branch that never compiles. The cacheentry 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.
SearchCachedDocumentsAsyncfollows the shape of the C# and VB service: priority documents and theprojects 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:
FSharpNavigableItemsCache, the per-document parse cache this extendsSymbolHelpers.searchThrottleandCancellableTask.forEachThrottledsearchedIn, the multi-target instance filterHasConditionalDirectivesis not in any open PR yet. As those merge, this diff shrinks to its owncommit; until then a branch off
maincannot carry this change, because it edits a typemaindoesnot have.
Verification
Three tests in
NavigateToSearchWhileLoadingTests.fs: the loading search finds a declaration wherethe accurate search raises for want of options; a file behind
#if FOOread under the wrong definesreturns nothing and does not poison the accurate search that follows once
--define:FOOarrives; andevery 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: 7301passed, 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
ProcessOrderedProjectsAsync→NavigateToSearcher.SearchCachedDocumentsAsync→ the externalaccess 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