Answer the Navigate To search that runs while the solution loads - #20492
xperiandri wants to merge 12 commits into
Conversation
✅ Release notes checked
Warning No PR link found in some release notes, please consider adding it.
|
ddffeff to
6bf2a72
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.
|
Moved out of the PR description per the pr-description guidance — how this was verified, since the branch cannot build in CI yet. Against a local build of Roslyn containing dotnet/roslyn#85213: Checked live as well, with that Roslyn build deployed to the experimental hive: on a 26-project mixed C#/F# solution, breaking in the F# implementation gives the stack |
6bf2a72 to
603ceb0
Compare
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>
603ceb0 to
b3b3cdc
Compare
`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>
Navigate To runs a search of its own while a solution is still loading, and F# contributes nothing to it: that phase is dispatched 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 is a parse away and needs no type checking. What it lacked is the project's compilation options, which do not exist yet during load, so the accurate parse entry point raises; the quick one parses with whatever parsing options the project system has already produced, or defaults — a dictionary read, no reactor, no I/O. Those defines can be the wrong ones, and the document's version does not change when the real options arrive, so a cache entry now records whether it was approximate: the accurate path refuses those and reparses, while the loading path takes either, since its contract allows out-of-date results. Otherwise a declaration behind
#ifcould be missed, or reported from a branch that never compiles.The search follows the shape of the C# and VB service: priority documents and the projects holding them first, results reported per document rather than per project so the first ones appear while the rest are still parsing, and every project's parses taking turns on one throttle that leaves a core free, so a search during load cannot take the cores away from the load itself.
Stacked on #20409, which adds the per-document parse cache this extends; once that merges the diff is two commits — giving the quick parsing options the file they parse, and the change itself.
🤖 Generated with Claude Code