Skip to content

Offer F# declarations to the Copilot chat "#" mention picker - #20409

Open
xperiandri wants to merge 5 commits into
dotnet:mainfrom
xperiandri:copilot-mentions
Open

Offer F# declarations to the Copilot chat "#" mention picker#20409
xperiandri wants to merge 5 commits into
dotnet:mainfrom
xperiandri:copilot-mentions

Conversation

@xperiandri

Copy link
Copy Markdown
Contributor

Description

GitHub Copilot Chat's # mention picker in Visual Studio lets you attach a symbol as context. Copilot's built-in provider reads symbols straight off the Roslyn Compilation, which F# projects do not have, so F# types, modules, members and values never showed up there.

This adds FSharpCopilotContextProvider, a brokered service proffered from FSharp.Editor that implements Copilot's ICopilotContextProvider / ICopilotMentionQueryable / ICopilotMentionBatchQueryable contracts directly, backed by the existing NavigateTo parse-tree cache (no project-wide typecheck needed, so the picker answers as fast as you type). A picked mention resolves by fully qualified name against the current solution and attaches the whole declaration — doc comment included — as its snippet.

Solution-wide document scanning in search/declarationsOf runs across documents concurrently, throttled the same way FindReferencesAsync throttles its per-document typechecks, so a query does not launch a parse per document all at once.

FSharpNavigableItemsCache's per-document cache entries move to struct tuples on this hot, per-keystroke path, and its null-workspace check moves to a match per this repo's conventions.

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes
  • Release notes entry updated

@github-actions

github-actions Bot commented Aug 31, 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:

Change path Release notes path Description
`vsintegration/src` docs/release-notes/.VisualStudio/18.vNext.md

@xperiandri

Copy link
Copy Markdown
Contributor Author
image image

@github-actions github-actions Bot added ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Restore Tooling check: PR touches NuGet packages or feeds labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Build-Infra, Affects-Restore
Affects-Build-Infra: adds PackageReference and EmbeddedResource to fsproj
Affects-Restore: new package version in eng/Packages.props

Generated by PR Tooling Safety Check · opus46 5.3M ·

@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.

Really nice piece of work. The design is clean, the comments explain the why behind every non-obvious choice, and it fills a real gap — F# projects have no Roslyn Compilation, so Copilot's built-in provider never saw F# symbols.

What stood out as excellent

  • Separation of concerns. CopilotSymbolQuery holds all the lookup logic and takes a Solution directly, so it's unit-tested with no VS workspace, while FSharpCopilotContextProvider stays a thin brokered-service adapter. That split is exactly why the tests read so well.
  • No project-wide typecheck. Reusing the NavigateTo parse-tree cache (now cleanly extracted as FSharpNavigableItemsCache, MEF-Shared so both consumers share one instance) keeps the picker responsive per keystroke.
  • Throttling via whenAllThrottled ProcessorCount mirrors FindReferencesAsync, so a query doesn't launch a parse-per-document storm.
  • Exhaustive mappings. symbolContextType/imageId cover all 11 NavigableItemKind cases — no partial-match surprises.
  • The cache extraction preserves the backtick/operator substring-match fallback verbatim; the struct-tuple change on that per-keystroke path and the nullmatch conversion are tidy.
  • Release note added; tests cover search, dedup, snippet extent, doc-comment inclusion, kind mapping, and the snippet-location round-trip.

Suggestions (none blocking)

  1. Exception safety at package load (LanguageService.fs). The registration task handles a null proxy (Copilot absent), but only that. If GetProxyAsync/RegisterContextProviderAsync throws — e.g. a Copilot contract-version mismatch, given you're compile-pinned to 18.9.918 but bind-redirect to whatever VS ships — the exception escapes the afterPackageLoadedTasks task. Worth confirming AddTask(false, …) isolates a faulting task, or wrapping the body in try/with so a Copilot hiccup can't perturb F# package load.

  2. Batch query is sequential, O(all docs) per query (QueryMentionBatchAsync). for query in queries do let! … = queryMentions query runs one full-solution scan per query, serially. Batches are usually tiny so it's fine in practice, but if Copilot ever sends several, they could be de-duplicated or run through the same throttle rather than back-to-back.

  3. One-line declarations drop their doc comment. In definitionLines, a construct with no body scope (/// doc + let x = 1) falls through to declarationLine, item.Range.EndLine, so its doc comment isn't captured — unlike the multi-line path, which deliberately reaches back over the doc comment. Minor; a follow-up could widen the one-line case to include an immediately-preceding /// block.

  4. Nit: the test's hardcoded "C:\\test.fs" is fine for Windows-only VS tests but couples to RoslynTestHelpers' internal path.

I reviewed statically and confirmed the in-tree helpers (whenAllThrottled, chooseV/tryHeadV/toImmutableArray, ValueOption.ofNullable) and the NavigableItemKind shape; I didn't run a VS-hosted build, so the Microsoft.VisualStudio.Copilot contract surface is taken on faith from the package reference.

Only item 1 feels worth a second look before merge. Thanks for this — it's going to be a delightful quality-of-life win for F# users in the Copilot picker. 🎉

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Sep 2, 2026
@T-Gro
T-Gro self-requested a review September 2, 2026 14:44
xperiandri and others added 3 commits September 2, 2026 21:17
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>
xperiandri and others added 2 commits September 3, 2026 19:24
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>
@xperiandri

Copy link
Copy Markdown
Contributor Author

#20443 has some improvements beneficial for this PR
@T-Gro let's proceed with it first

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

Labels

⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Restore Tooling check: PR touches NuGet packages or feeds AI-reviewed PR reviewed by AI review council

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

2 participants