Skip to content

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

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

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

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Draft: it cannot build against the Roslyn this repository references yet. dotnet/roslyn#85213, the contract this implements, merged on 2026-09-09, but main still builds against Roslyn 5.12.0-1.26428.6 (2026-08-28), which predates it. It needs the next dependency update from dotnet/roslyn.

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 #if could 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

@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

✅ Release notes checked


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

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖🕵️ 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.

@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Sep 15, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author

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: NavigateToSearchWhileLoadingTests, three tests — 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. With the approximate check removed, the first two fail. With NavigateToSearchServiceTests and CopilotContextProviderTests, 31 pass. The full FSharp.Editor.Tests run: 7219 passed, 2 skipped, 4 failed — fsiShouldTriggerCompletionInFsxFile and three type provider tests, which fail the same way on this branch's base without its commits. FSharp.Editor builds with -p:Optimize=true.

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 ProcessOrderedProjectsAsync → NavigateToSearcher.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.

@xperiandri
xperiandri force-pushed the feature/navigate-to-while-loading branch from 6bf2a72 to 603ceb0 Compare September 22, 2026 11:34
xperiandri and others added 11 commits September 26, 2026 03:01
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
xperiandri force-pushed the feature/navigate-to-while-loading branch from 603ceb0 to b3b3cdc Compare September 26, 2026 01:01
`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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants