Answer the Navigate To search that runs while the solution loads - #20492
Draft
xperiandri wants to merge 11 commits into
Draft
Answer the Navigate To search that runs while the solution loads#20492xperiandri wants to merge 11 commits into
xperiandri wants to merge 11 commits into
Conversation
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.
|
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>
Package load runs its tasks back to back on a single loop, so an exception from the Copilot registration task escaped into F# package load. A Copilot contract version the installed build does not serve would have taken the whole package down; catch and log instead, leaving cancellation alone. A doc comment is only reported as an outlining scope once it spans several lines, so a one-line "///" in front of a declaration was invisible to the scope search and dropped from the snippet. Walk back over the preceding "///" lines directly. Batch mention queries scanned the solution once per query, serially. Distinct search texts now scan concurrently and repeated ones share a single scan. The snippet-location test asserted a hardcoded "C:\test.fs" rather than asking the solution where its document lives. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolving a picked mention walks every declaration in every document of the solution, and asked each one for its dotted path as a fresh string purely to compare it. Compare against the container and name in place instead, so the scan allocates nothing per declaration. The doc-comment probe trimmed each candidate line into a new string for the same reason. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every consumer of Structure.getOutliningRanges built its sourceLines array by calling ToString() per line, allocating a fresh string for the entire file on every outlining pass - once per keystroke for the editor's block structure, and once per resolved Copilot mention for the snippet extent. getOutliningRanges now takes ReadOnlyMemory<char>[] and slices the already-materialized source text once (SourceText.GetLinesAsMemory()) instead. ReadOnlySpanCharExtensions in illib mirrors the existing Ordinal string helpers so span call sites read the same way string call sites do. A local recursive function closing over a ReadOnlySpan<char>-typed sibling cannot be compiled - the CLR disallows instantiating FSharpFunc<ReadOnlySpan<char>, _> as a closure field (FS0412) - so commentTypeOf moves to module scope, next to the CommentType it classifies. StructureTests.fs slices its own lines the same way at the call site, and FSharp.Compiler.Service.Tests needs a direct System.Memory PackageReference: FSharp.Compiler.Service's own reference to it is only transitive through the net472 ProjectReference's SetTargetFramework override, mirroring the FSharp.Core pin already in this project for the same reason. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CommentList kept a copy of every comment line next to its line number, but the number alone identifies the line in the source array the function already holds, and only the first and last lines of a group are ever read back to compute the fold's columns. Store the numbers and index the source at the end, so grouping comments allocates no tuple per line. 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.
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>
xperiandri
force-pushed
the
feature/navigate-to-while-loading
branch
from
September 11, 2026 15:52
ddffeff to
6bf2a72
Compare
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 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.
What you are looking at
The branch is
mainplus the commits of #20409, which addsFSharpNavigableItemsCache, theper-document parse cache this extends, and two commits of its own:
parsing options with no source files, and
ParseFilethrows looking for the last compiland. Nothingparsed with them before a project had loaded; the loading search does.
Once #20409 merges, the diff is those two commits.
Verification
Against a local build of Roslyn that contains dotnet/roslyn#85213:
NavigateToSearchWhileLoadingTests, three tests: the loading search finds a declaration where theaccurate 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;and every project is reported complete exactly once whether or not anything was found in it. With
the approximate check removed, the first two fail.
NavigateToSearchServiceTestsandCopilotContextProviderTests, 31 pass. The fullFSharp.Editor.Testsrun: 7219 passed, 2 skipped, 4 failed —fsiShouldTriggerCompletionInFsxFileand three type provider tests, which fail the same way on this branch's base without its commits.
FSharp.Editorbuilds with-p:Optimize=true.Checked live as well, on the branch this was first written on, with the Roslyn build deployed to the
experimental hive: 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