Skip to content

Add F# code snippets (Ctrl+K,Ctrl+X / Ctrl+K,Ctrl+S) - #20521

Open
xperiandri wants to merge 6 commits into
dotnet:mainfrom
xperiandri:feature/vs-code-snippets
Open

xperiandri wants to merge 6 commits into
dotnet:mainfrom
xperiandri:feature/vs-code-snippets

Conversation

@xperiandri

@xperiandri xperiandri commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #1498

Adds Visual Studio code-snippet support for F#: Ctrl+K,Ctrl+X inserts a snippet, Ctrl+K,Ctrl+S surrounds a selection, and a snippet shortcut expands on Tab. Ships a built-in catalog matching C#'s (declarations, members, control flow, computation expressions); ctor/equals fill in the enclosing type name and match generates the cases of the union or enum it is given.

Verified by hand in the experimental hive in addition to the added tests.

Not in scope: snippet shortcuts as an IntelliSense completion item (as C# offers), localized snippet folders, <Imports>/<References> support, and snippets for test methods.

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes — not applicable, no existing path changes
  • Release notes entry updated

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

✅ Release notes checked


✅ Found changes and release notes in following paths:

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

@xperiandri
xperiandri marked this pull request as ready for review September 11, 2026 09:51
@xperiandri
xperiandri requested a review from a team as a code owner September 11, 2026 09:51
@xperiandri xperiandri changed the title Code snippets for F# in Visual Studio (Ctrl+K,Ctrl+X / Ctrl+K,Ctrl+S) Code snippets for F# in Visual Studio (Ctrl+K,Ctrl+X / Ctrl+K,Ctrl+S) Sep 11, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author
image image image image

@github-actions github-actions Bot added the ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure label Sep 11, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager ⚠️ Affects-Test-Tooling Tooling check: PR touches test framework infrastructure labels Sep 11, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author

/azp run fsharp-ci

@azure-pipelines

Copy link
Copy Markdown
Commenter does not have sufficient privileges for PR 20521 in repo dotnet/fsharp

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

🤖 🕵️ AI review — verify independently.

Comment thread vsintegration/src/FSharp.Editor/Snippets/SnippetFunctions.fs Outdated
Comment thread vsintegration/src/FSharp.Editor/Snippets/SnippetIndentation.fs
Comment thread vsintegration/src/FSharp.Editor/Snippets/SnippetFunctions.fs Outdated
Comment thread vsintegration/src/FSharp.Editor/Snippets/SnippetFunctions.fs Outdated
Comment thread vsintegration/Vsix/VisualFSharpFull/snippets/1033/FSharp/lock.snippet Outdated
@T-Gro
T-Gro self-requested a review September 11, 2026 15:30
@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Sep 11, 2026

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

🤖🕵️ If this fixes an issue or implements an RFC/suggestion, link it (Fixes #... when applicable). Otherwise, give a short management-level summary in simplified technical English: what user scenario improves and what this achieves.

Please apply this PR-description guidance. Remove the implementation inventory already visible in Files, but keep necessary scope, compatibility, and dependency caveats.

@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Sep 14, 2026
@xperiandri xperiandri changed the title Code snippets for F# in Visual Studio (Ctrl+K,Ctrl+X / Ctrl+K,Ctrl+S) Add F# code snippets (Ctrl+K,Ctrl+X / Ctrl+K,Ctrl+S) Sep 14, 2026
@xperiandri

Copy link
Copy Markdown
Contributor Author

Rewrote per the guidance — description is now Fixes #1498 + one paragraph on user-visible behavior + scope/compatibility caveats, implementation inventory removed.

@xperiandri
xperiandri force-pushed the feature/vs-code-snippets branch 2 times, most recently from 50be8a5 to 77bccb8 Compare September 17, 2026 22:51
@xperiandri
xperiandri requested a review from T-Gro September 18, 2026 09:28

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

🤖🕵️ AI review — verify independently.

Comment thread vsintegration/src/FSharp.Editor/Snippets/SnippetExpansionClient.fs
Comment thread vsintegration/src/FSharp.Editor/Snippets/SnippetExpansionClient.fs
Comment thread vsintegration/src/FSharp.Editor/Snippets/SnippetFunctions.fs Outdated
@xperiandri
xperiandri force-pushed the feature/vs-code-snippets branch from 77bccb8 to c337cc0 Compare September 21, 2026 10:14
@T-Gro T-Gro added the vsintegration-only Changes only Visual Studio integration, plus optional docs, release notes, or tests label Sep 21, 2026
@xperiandri
xperiandri force-pushed the feature/vs-code-snippets branch from 2dacdf1 to 080f662 Compare September 21, 2026 17:56
@xperiandri
xperiandri requested a review from T-Gro September 21, 2026 21:03
@github-actions

This comment has been minimized.

@xperiandri
xperiandri force-pushed the feature/vs-code-snippets branch from 080f662 to e7775f7 Compare September 23, 2026 18:38
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Tooling Safety Check — Affects-Design-Time
Affects-Design-Time: Modifies FCS services used by editor tooling.

Generated by PR Tooling Safety Check · gpt56 2.3M ·

xperiandri and others added 6 commits September 24, 2026 13:36
Insert Snippet and Surround With have had nothing to offer in an F# file: the Code Snippets
Manager has no F# entry and this repository contains no `.snippet` file at all.

Adds 40 snippets covering the part of the C# set that has an F# analogue - declarations,
members, control flow, computation expressions - together with the registration and packaging
that lets Visual Studio find them. `Languages\CodeExpansions\FSharp` is written into the
pkgdef rather than produced by `ProvideLanguageCodeExpansionAttribute`, which does not expose
the `Package` value that `DisplayName` resolves against; C#, VB, XAML, XML and TypeScript all
register by hand for the same reason. Only 1033 is registered, and outright rather than as
`%LCID%`: registering both would enumerate every snippet twice on an English VS.

The shipped directory is `Snippets\1033\FSharp`, not `Visual F#`, because a '#' in a VSIX part
URI reads as a URI fragment and the packaging step refuses it. `SnippetsIndex.xml` supplies the
folder name the Code Snippets Manager shows.

Bodies are authored at column 0 with 4-space relative indentation - absolute indentation is
applied at insertion time - and every snippet carries an explicit `$end$`, which is what lets
the expansion client avoid reading the snippet XML back out of the live session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Insert Snippet (Ctrl+K,Ctrl+X), Surround With (Ctrl+K,Ctrl+S), Tab expansion of a snippet
shortcut, and the keys that drive a live expansion session.

Nothing here reuses Roslyn: its snippet stack is `internal` under
`LanguageServices.Implementation.Snippets` with no ExternalAccess surface, so F# writes its own
`IVsExpansionClient` the way it already writes its own brace completion. The commands come in
through one MEF `ICommandHandler<_>` part, ordered after the completion handler so that Tab
still commits an open completion list first.

Indentation is the F#-specific part. The expansion engine inserts snippet text verbatim, and
C# gets away with that because Roslyn's formatter reflows the result afterwards; F# has no
formatter, so `FormatSpan` computes the columns. That arithmetic lives in `SnippetIndentation`,
free of editor types so that it can be tested on its own - the policy is where the mistakes
live, not the buffer edit that applies it. A directive wrapper is its own line kind:
`#if`/`#else`/`#endif` and the scoped `#nowarn`/`#warnon` pair read at the left margin whatever
they wrap, so the code they cover keeps the column it had.

Two things worth knowing for anyone reading `IVsExpansionClient` next to Roslyn's:
`tsInsertPos` is the range `InsertNamedExpansion` replaces, so handing it the selection deletes
the code a SurroundsWith snippet was meant to wrap; and `GetFieldSpan "selected"` does not
answer for that special literal, so the substituted range is derived from the template's own
`$selected$` line plus the line count the command handler took before the insertion.

`ClassName()` and `GenerateMatchCases()` back the `ctor`, `equals` and `match` snippets. Both
are synchronous COM callbacks, so they block; `ClassName()` blocks on a parse and
`GenerateMatchCases()` on the stale-tolerant check-results path, falling back to a visible
`| _ -> ()` rather than waiting unbounded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A RequireQualifiedAccess union rejects a bare case pattern (`A`, not `U.A`) - the
generated pattern binds a fresh variable instead of testing the case, so it takes the
first arm regardless of the actual value. matchRulesFor now qualifies with the
entity's DisplayName when the union carries the attribute, the same way it already
does for enums.

ClassName() used the navigation item's LogicalName, which is qualified by every
enclosing module ("Outer.C") - it does not resolve from a constructor sitting inside
C's own scope. Strips to the name after the last '.'.

GenerateMatchCases() resolved the lexer symbol nearest the field's end position,
which is whatever identifier happens to sit there - for `f x`, that is `x`, not the
call `f x`. Reads the type the checker captured for the field's whole span instead
(TryGetCapturedType), so it matches the expression's own result type regardless of
its shape.

Surround With reindented every non-first selected line uniformly, including a line
that is itself inside a multi-line string continued from an earlier selected line -
inserting indentation there changes the string's value, not just its position.
classifyLines now threads the lexer's color state across the span and classifies
such a line as InsideString, left untouched like a blank line.

lock.snippet closed the lambda on the same line as $selected$$end$, so a selection
whose last line ends in a trailing // comment swallowed the closing paren into the
comment, leaving the call unclosed. Moved onto its own line, matching how the other
wrapping snippets already close.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A match on a union or enum now spells its cases with the shortest qualifier that resolves
at the insertion point, asked of the checked file the way the simplify-names analyzer
asks it, instead of assuming `DisplayName.` is reachable: a `[<RequireQualifiedAccess>]`
union in an unopened module produced `U.A`, which does not resolve. The expression's
captured type is used as it stands, so matching a function value no longer generates
the arms of its result type.

Indentation is measured in visual columns. A snippet expanded after a tab took the
character count as its column and came out one space in, and Surround With over
tab-indented code wrapped it at the wrong column.

A cancelled Surround With never reports back, so its pending selection was still set
when the next shortcut expanded; the shortcut path now clears it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expands and surrounds like `task`, for code that has to run off the caller's context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 24, 2026 11:36
@xperiandri
xperiandri force-pushed the feature/vs-code-snippets branch from e7775f7 to 80593e5 Compare September 24, 2026 11:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved insertion-crash, interface-snippet, line-ending, and expansion-session issues remain.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Low severity

Open (2)
What changed in this PR

Adds Visual Studio snippet support for F#, including insertion, surround-with, Tab expansion, dynamic type-name and match-case generation, packaging, tests, localization, and release notes.

Changes:

  • Adds and registers 41 built-in F# snippets.
  • Implements snippet commands, expansion sessions, indentation, and dynamic functions.
  • Adds catalog and indentation tests plus localized resources.
File Description
vsintegration/​Vsix/​VisualFSharpFull/​VisualFSharp.Core.targets Packages snippet assets.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​SnippetsIndex.xml Registers the snippet catalog.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​while.snippet Adds the while snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​use.snippet Adds the use snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​tryf.snippet Adds the try/finally snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​try.snippet Adds the try snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​task.snippet Adds the task snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​struct.snippet Adds the struct snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​seq.snippet Adds the sequence snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​record.snippet Adds the record snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​propfull.snippet Adds the full property snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​prop.snippet Adds the property snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​pp_if.snippet Adds the conditional compilation snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​pfn.snippet Adds the function snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​override.snippet Adds the override snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​nowarn.snippet Adds the warning suppression snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​module.snippet Adds the module snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​member.snippet Adds the member snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​matchvo.snippet Adds the value-option match snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​matcht.snippet Adds the tuple match snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​matchr.snippet Adds the record match snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​matcho.snippet Adds the option match snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​match.snippet Adds the dynamic match snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​main.snippet Adds the main function snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​lock.snippet Adds the lock snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​interface.snippet Adds the interface declaration snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​ife.snippet Adds the if/else expression snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​iface.snippet Adds the interface implementation snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​if.snippet Adds the if snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​forr.snippet Adds the range loop snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​for.snippet Adds the for loop snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​ext.snippet Adds the extension snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​exn.snippet Adds the exception snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​equals.snippet Adds the equality override snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​enum.snippet Adds the enum snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​du.snippet Adds the discriminated union snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​dispose.snippet Adds the disposal snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​ctor.snippet Adds the constructor snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​class.snippet Adds the class snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​backgroundTask.snippet Adds the background task snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​attribute.snippet Adds the attribute snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​async.snippet Adds the async snippet.
vsintegration/​Vsix/​VisualFSharpFull/​snippets/​1033/​FSharp/​abstract.snippet Adds the abstract member snippet.
vsintegration/​Vsix/​RegisterFsharpPackage.pkgdef Registers snippet paths and language metadata.
vsintegration/​tests/​FSharp.Editor.Tests/​Snippets/​SnippetIndentationTests.fs Tests snippet indentation.
vsintegration/​tests/​FSharp.Editor.Tests/​Snippets/​SnippetCatalogTests.fs Validates the snippet catalog.
vsintegration/​tests/​FSharp.Editor.Tests/​FSharp.Editor.Tests.fsproj Includes snippet tests and assets.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.zh-Hant.xlf Adds Traditional Chinese resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.zh-Hans.xlf Adds Simplified Chinese resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.tr.xlf Adds Turkish resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.ru.xlf Adds Russian resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.pt-BR.xlf Adds Brazilian Portuguese resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.pl.xlf Adds Polish resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.ko.xlf Adds Korean resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.ja.xlf Adds Japanese resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.it.xlf Adds Italian resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.fr.xlf Adds French resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.es.xlf Adds Spanish resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.de.xlf Adds German resources.
vsintegration/​src/​FSharp.Editor/​xlf/​FSharp.Editor.cs.xlf Adds Czech resources.
vsintegration/​src/​FSharp.Editor/​Snippets/​SnippetIndentation.fs Implements expansion indentation.
vsintegration/​src/​FSharp.Editor/​Snippets/​SnippetFunctions.fs Implements dynamic snippet functions.
vsintegration/​src/​FSharp.Editor/​Snippets/​SnippetExpansionClient.fs Manages expansion sessions.
vsintegration/​src/​FSharp.Editor/​Snippets/​SnippetCommandHandler.fs Handles snippet commands and shortcuts.
vsintegration/​src/​FSharp.Editor/​LanguageService/​FSharpEditorFactory.fs Adds the command-handler name.
vsintegration/​src/​FSharp.Editor/​FSharp.Editor.resx Adds command resources.
vsintegration/​src/​FSharp.Editor/​FSharp.Editor.fsproj Includes snippet implementation files.
vsintegration/​src/​FSharp.Editor/​Common/​Vs.fs Exposes the expansion manager.
vsintegration/​src/​FSharp.Editor/​Common/​Extensions.fs Adds line-ending detection.
docs/​release-notes/​.VisualStudio/​18.vNext.md Documents the feature.

Comment on lines +48 to +49
for offset in 0 .. length - 1 do
column <- SnippetIndentation.advanceColumn tabSize column snapshot[start + offset]
Comment on lines +181 to +209
/// The match rules covering the union or enum `$expression$` evaluates to, or ValueNone for any
/// other type.
let tryGetMatchRules (document: Document) (span: VsTextSpan) =
cancellableTask {
let! _, checkResults = document.GetFSharpParseAndCheckResultsAsync userOpName
let! ct = CancellableTask.getCancellationToken ()
let! sourceText = document.GetTextAsync ct

let range =
Range.mkRange
document.FilePath
(Position.mkPos (span.iStartLine + 1) span.iStartIndex)
(Position.mkPos (span.iEndLine + 1) span.iEndIndex)

let position = sourceText.Lines[span.iEndLine].Start + span.iEndIndex

let rules =
match checkResults.TryGetCapturedType range with
| Some fsharpType ->
let fsharpType = fsharpType.StripAbbreviations()

if fsharpType.HasTypeDefinition then
matchRulesFor checkResults range.Start fsharpType.TypeDefinition
else
Seq.empty
| None -> Seq.empty

return
match String.Join(sourceText.LineBreakAt position, rules) with
@github-actions github-actions Bot added the ⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain label Sep 24, 2026

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

⚠️ Affects-Bootstrap Tooling check: PR touches compiler bootstrap chain ⚠️ Affects-Build-Infra Tooling check: PR touches build infrastructure ⚠️ Affects-Design-Time Tooling check: PR touches type providers or dependency manager ⚠️ Affects-Test-Tooling Tooling check: PR touches test framework infrastructure AI-reviewed PR reviewed by AI review council vsintegration-only Changes only Visual Studio integration, plus optional docs, release notes, or tests

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Suggestion: support F# Snippets

3 participants