diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md
index ba03f663967..11a864f16fb 100644
--- a/docs/release-notes/.VisualStudio/18.vNext.md
+++ b/docs/release-notes/.VisualStudio/18.vNext.md
@@ -2,6 +2,7 @@
* Code-fixes for FS3888 (compiler-semantic attribute on the `.fs` but not the `.fsi`): copy the attribute into the `.fsi`, or remove it from the `.fs`. ([Issue #19560](https://github.com/dotnet/fsharp/issues/19560), [PR #19880](https://github.com/dotnet/fsharp/pull/19880))
* Expand `` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188))
+* Refactoring to convert a lambda that only reads a member of its parameter (`fun x -> x.Prop`, `fun x -> x.M(y)`, `fun x -> x.Items[0]`) into the F# 8 shorthand `_.Prop`, and a `_.Prop` shorthand back into a `fun` lambda. ([Issue #16234](https://github.com/dotnet/fsharp/issues/16234), [PR #20535](https://github.com/dotnet/fsharp/pull/20535))
### Fixed
diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
index 319bdd5a264..e2df0ed12e3 100644
--- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
+++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
@@ -105,6 +105,7 @@
+
diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx
index 1f1f632d770..cb93aa0cc7d 100644
--- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx
+++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx
@@ -368,4 +368,10 @@ Use live (unsaved) buffers for analysis
Returns:
+
+ Convert to '_.' shorthand lambda
+
+
+ Convert to 'fun' lambda
+
\ No newline at end of file
diff --git a/vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs b/vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs
new file mode 100644
index 00000000000..96bbff82e7d
--- /dev/null
+++ b/vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs
@@ -0,0 +1,187 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+namespace Microsoft.VisualStudio.FSharp.Editor
+
+open System
+open System.Collections.Generic
+open System.Composition
+
+open Microsoft.CodeAnalysis.CodeActions
+open Microsoft.CodeAnalysis.CodeRefactorings
+open Microsoft.CodeAnalysis.Text
+open Microsoft.VisualStudio.FSharp.Editor.Telemetry
+
+open FSharp.Compiler.Syntax
+open FSharp.Compiler.SyntaxTrivia
+open FSharp.Compiler.Text
+
+open CancellableTasks
+
+[]
+module private DotLambdaConversion =
+
+ []
+ type Conversion =
+ {
+ Title: string
+ Direction: string
+ Change: TextChange
+ }
+
+ let hasName (name: string) (ident: Ident) =
+ String.Equals(ident.idText, name, StringComparison.Ordinal)
+
+ // The shapes SyntaxTreeOps.pushUnaryArg accepts under `_.`, with the parameter as the head of the chain.
+ let rec tryChainRoot expr =
+ match expr with
+ | SynExpr.LongIdent(longDotId = SynLongIdent(id = root :: _ :: _; dotRanges = dot :: _)) when
+ Position.posEq dot.Start root.idRange.End
+ ->
+ ValueSome root
+ | SynExpr.DotGet(expr = inner)
+ | SynExpr.DotIndexedGet(objectExpr = inner)
+ | SynExpr.TypeApp(expr = inner)
+ | SynExpr.App(flag = ExprAtomicFlag.Atomic; isInfix = false; funcExpr = inner) -> tryChainRoot inner
+ | _ -> ValueNone
+
+ let occurrencesOf (name: string) (body: SynExpr) =
+ (0, [ SyntaxNode.SynExpr body ])
+ ||> SyntaxNodes.fold (fun count _ node ->
+ match node with
+ | SyntaxNode.SynExpr(SynExpr.Ident ident)
+ | SyntaxNode.SynExpr(SynExpr.LongIdent(longDotId = SynLongIdent(id = ident :: _)))
+ | SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident, _))) when hasName name ident -> count + 1
+ | _ -> count)
+
+ let parameterNameFor (body: SynExpr) =
+ let used =
+ (HashSet(StringComparer.Ordinal), [ SyntaxNode.SynExpr body ])
+ ||> SyntaxNodes.fold (fun names _ node ->
+ match node with
+ | SyntaxNode.SynExpr(SynExpr.Ident ident)
+ | SyntaxNode.SynExpr(SynExpr.LongIdent(longDotId = SynLongIdent(id = ident :: _)))
+ | SyntaxNode.SynPat(SynPat.Named(ident = SynIdent(ident, _))) -> ignore (names.Add ident.idText)
+ | _ -> ()
+
+ names)
+
+ Seq.initInfinite (fun i -> if i = 0 then "x" else $"x{i}")
+ |> Seq.find (used.Contains >> not)
+
+ let isInQuotation (path: SyntaxVisitorPath) =
+ path
+ |> List.exists (function
+ | SyntaxNode.SynExpr(SynExpr.Quote _) -> true
+ | _ -> false)
+
+ let isAppliedDirectly (lambda: SynExpr) (path: SyntaxVisitorPath) =
+ match path with
+ | SyntaxNode.SynExpr(SynExpr.Paren(expr = inner) as paren) :: SyntaxNode.SynExpr(SynExpr.App(funcExpr = func)) :: _ ->
+ obj.ReferenceEquals(inner, lambda) && obj.ReferenceEquals(func, paren)
+ | SyntaxNode.SynExpr(SynExpr.App(funcExpr = func)) :: _ -> obj.ReferenceEquals(func, lambda)
+ | _ -> false
+
+ let spanOf (sourceText: SourceText) (m: range) =
+ RoslynHelpers.FSharpRangeToTextSpan(sourceText, m)
+
+ let isBlankBetween (sourceText: SourceText) start finish =
+ start <= finish
+ && String.IsNullOrWhiteSpace(sourceText.ToString(TextSpan.FromBounds(start, finish)))
+
+ let toShorthand (sourceText: SourceText) (path: SyntaxVisitorPath) (lambda: SynExpr) (root: Ident) =
+ let lambdaSpan = spanOf sourceText lambda.Range
+ let rootEnd = (spanOf sourceText root.idRange).End
+
+ match path with
+ | SyntaxNode.SynExpr(SynExpr.Paren(expr = inner; leftParenRange = leftParen; rightParenRange = Some rightParen; range = parenRange) as paren) :: SyntaxNode.SynExpr(SynExpr.App(
+ flag = ExprAtomicFlag.NonAtomic; isInfix = false; argExpr = arg)) :: _ when
+ obj.ReferenceEquals(inner, lambda)
+ && obj.ReferenceEquals(arg, paren)
+ && isBlankBetween sourceText (spanOf sourceText leftParen).End lambdaSpan.Start
+ && isBlankBetween sourceText lambdaSpan.End (spanOf sourceText rightParen).Start
+ ->
+ let chain = sourceText.ToString(TextSpan.FromBounds(rootEnd, lambdaSpan.End))
+ TextChange(spanOf sourceText parenRange, $"_{chain}")
+ | _ -> TextChange(TextSpan.FromBounds(lambdaSpan.Start, rootEnd), "_")
+
+ let toLambda (sourceText: SourceText) (path: SyntaxVisitorPath) (body: SynExpr) (range: range) (trivia: SynExprDotLambdaTrivia) =
+ let name = parameterNameFor body
+ let lambda = $"fun {name} -> {name}"
+ let underscoreStart = (spanOf sourceText trivia.UnderscoreRange).Start
+ let dotStart = (spanOf sourceText trivia.DotRange).Start
+
+ match path with
+ | SyntaxNode.SynExpr(SynExpr.Paren _) :: _
+ | SyntaxNode.SynBinding _ :: _ -> TextChange(TextSpan.FromBounds(underscoreStart, dotStart), lambda)
+ | _ ->
+ let span = spanOf sourceText range
+ let chain = sourceText.ToString(TextSpan.FromBounds(dotStart, span.End))
+ TextChange(span, $"({lambda}{chain})")
+
+ let tryFind (sourceText: SourceText) (position: pos) (parseTree: ParsedInput) =
+ (position, parseTree)
+ ||> ParsedInput.tryPickLast (fun path node ->
+ match node with
+ | SyntaxNode.SynExpr(SynExpr.Lambda(
+ fromMethod = false
+ parsedData = Some([ SynPat.Named(ident = SynIdent(parameter, _); isThisVal = false; accessibility = None) ], body)) as lambda) when
+ not (isInQuotation path) && not (isAppliedDirectly lambda path)
+ ->
+ match tryChainRoot body with
+ | ValueSome root when hasName parameter.idText root && occurrencesOf parameter.idText body = 1 ->
+ Some
+ {
+ Title = SR.ConvertToShorthandLambda()
+ Direction = "shorthand"
+ Change = toShorthand sourceText path lambda root
+ }
+ | _ -> None
+
+ | SyntaxNode.SynExpr(SynExpr.DotLambda(expr = body; range = range; trivia = trivia)) when
+ not body.IsArbExprAndThusAlreadyReportedError
+ ->
+ Some
+ {
+ Title = SR.ConvertToFunLambda()
+ Direction = "lambda"
+ Change = toLambda sourceText path body range trivia
+ }
+
+ | _ -> None)
+
+[]
+type internal FSharpConvertDotLambdaRefactoring [] () =
+ inherit CodeRefactoringProvider()
+
+ override _.ComputeRefactoringsAsync context =
+ cancellableTask {
+ let document = context.Document
+
+ if not document.IsFSharpSignatureFile then
+ let! cancellationToken = CancellableTask.getCancellationToken ()
+ let! sourceText = document.GetTextAsync cancellationToken
+
+ let! parseResults = document.GetFSharpParseResultsAsync(nameof FSharpConvertDotLambdaRefactoring)
+
+ let caret = sourceText.Lines.GetLinePosition context.Span.Start
+ let position = Position.mkPos (Line.fromZ caret.Line) caret.Character
+
+ match DotLambdaConversion.tryFind sourceText position parseResults.ParseTree with
+ | Some conversion ->
+ let changedDocument =
+ cancellableTask {
+ TelemetryReporter.ReportSingleEvent(
+ TelemetryEvents.RefactoringActivated,
+ [|
+ "name", box (nameof FSharpConvertDotLambdaRefactoring)
+ "direction", box conversion.Direction
+ |]
+ )
+
+ return document.WithText(sourceText.WithChanges conversion.Change)
+ }
+
+ context.RegisterRefactoring(CodeAction.Create(conversion.Title, changedDocument, conversion.Title))
+ | None -> ()
+ }
+ |> CancellableTask.startAsTask context.CancellationToken
diff --git a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs
index 230f4988438..a5bf5e6b4ef 100644
--- a/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs
+++ b/vsintegration/src/FSharp.Editor/Telemetry/TelemetryReporter.fs
@@ -18,6 +18,9 @@ module TelemetryEvents =
[]
let CodefixActivated = "codefixactivated"
+ []
+ let RefactoringActivated = "refactoringactivated"
+
[]
let Hints = "hints"
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf
index cd8c46bf705..3ef136d2536 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf
@@ -90,11 +90,21 @@ Navrhnout názvy pro nerozpoznané identifikátory;
Převést na anonymní záznam
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
Pro kontrolu nerovnosti použijte <>.
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
Pro kontrolu rovnosti použijte =.
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf
index bce1941f0b1..ca93d64614c 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf
@@ -90,11 +90,21 @@ Namen für nicht aufgelöste Bezeichner vorschlagen;
In anonymen Datensatz konvertieren
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
"<>" für die Überprüfung auf Ungleichheit verwenden
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
"=" für Gleichheitsüberprüfung verwenden
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf
index fa8cb62c422..4fc14b5665e 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf
@@ -90,11 +90,21 @@ Sugerir nombres para identificadores sin resolver;
Convertir en registro anónimo
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
Usar "<>" para la comprobación de desigualdad
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
Usar "=" para la comprobación de igualdad
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf
index e7ec71e839e..a4fd8ad0d5b 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf
@@ -90,11 +90,21 @@ Suggérer des noms pour les identificateurs non résolus ;
Convertir en enregistrement anonyme
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
Utiliser '<>' pour vérifier l'inégalité
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
Utiliser '=' pour vérifier l'égalité
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf
index 327a7ca362f..5bd08ff3e58 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf
@@ -90,11 +90,21 @@ Suggerisci i nomi per gli identificatori non risolti;
Converti in record anonimo
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
Usare '<>' per il controllo di disuguaglianza
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
Usare '=' per il controllo di uguaglianza
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf
index d45234c011a..dc75026bfd3 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf
@@ -90,11 +90,21 @@ Suggest names for unresolved identifiers;
匿名レコードに変換
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
非等値のチェックには '<>' を使用します
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
等値性のチェックには '=' を使用します
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf
index 3248e0641ea..833d2c81ab5 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf
@@ -90,11 +90,21 @@ Suggest names for unresolved identifiers;
익명 레코드로 변환
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
같지 않음 검사에 '<>' 사용
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
같음 검사에 '=' 사용
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf
index abc39f15da5..ff8463459ae 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pl.xlf
@@ -90,11 +90,21 @@ Sugeruj nazwy dla nierozpoznanych identyfikatorów;
Konwertuj na rekord anonimowy
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
Użyj operatora „<>” do sprawdzenia nierówności
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
Użyj znaku „=” w celu sprawdzenia równości
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf
index dfde43120f5..9e2d2e92599 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.pt-BR.xlf
@@ -90,11 +90,21 @@ Sugerir nomes para identificadores não resolvidos;
Converter em Registro Anônimo
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
Usar '<>' para a verificação de desigualdade
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
Usar '=' para verificação de igualdade
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf
index 47cda215312..66b7e0c9c65 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ru.xlf
@@ -90,11 +90,21 @@ Suggest names for unresolved identifiers;
Преобразовать в анонимную запись
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
Используйте "<>" для проверки на неравенство
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
Используйте "=" для проверки равенства
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf
index 58aa5d54c43..d5071371a7a 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.tr.xlf
@@ -90,11 +90,21 @@ Kullanılmayan değerleri analiz et ve bunlara düzeltmeler öner;
Anonim Kayda Dönüştür
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
Eşitsizlik denetimi için '<>' kullanın
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
Eşitlik denetimi için '=' kullan
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf
index 4fa703776fb..ddaa7832d82 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hans.xlf
@@ -90,11 +90,21 @@ Suggest names for unresolved identifiers;
转换为匿名记录
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
使用 "<>" 进行不相等检查
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
使用 "=" 进行同等性检查
diff --git a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf
index fd46ef9919a..fa5ebeecedf 100644
--- a/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf
+++ b/vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.zh-Hant.xlf
@@ -90,11 +90,21 @@ Suggest names for unresolved identifiers;
轉換為匿名記錄
+
+ Convert to 'fun' lambda
+ Convert to 'fun' lambda
+
+
Use '<>' for inequality check
使用 '<>' 進行不等式檢查
+
+ Convert to '_.' shorthand lambda
+ Convert to '_.' shorthand lambda
+
+
Use '=' for equality check
使用 '=' 檢查是否相等
diff --git a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj
index ecce1205b8c..4f5fc7ccc43 100644
--- a/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj
+++ b/vsintegration/tests/FSharp.Editor.Tests/FSharp.Editor.Tests.fsproj
@@ -74,6 +74,7 @@
+
diff --git a/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs
new file mode 100644
index 00000000000..e09ee2f0af0
--- /dev/null
+++ b/vsintegration/tests/FSharp.Editor.Tests/Refactors/ConvertDotLambdaTests.fs
@@ -0,0 +1,120 @@
+module FSharp.Editor.Tests.Refactors.ConvertDotLambdaTests
+
+open System
+
+open Microsoft.VisualStudio.FSharp.Editor
+
+open Xunit
+
+open FSharp.Editor.Tests.Refactors.RefactorTestFramework
+
+let private inModule (binding: string) =
+ $"""
+module M
+
+let r = {binding}
+"""
+
+let private caretAt (code: string) (marker: string) =
+ code.IndexOf(marker, StringComparison.Ordinal)
+
+let private refactored (code: string) (marker: string) =
+ use context = TestContext.CreateWithCode code
+
+ let document =
+ tryRefactor code (caretAt code marker) context (new FSharpConvertDotLambdaRefactoring())
+
+ (document.GetTextAsync() |> GetTaskResult).ToString()
+
+let private actionsIn (context: TestContext) (code: string) (marker: string) =
+ tryGetRefactoringActions code (caretAt code marker) context (new FSharpConvertDotLambdaRefactoring())
+
+[]
+[ List.map (fun x -> x.Prop)", "xs |> List.map _.Prop")>]
+[ x.A.B) xs", "List.map _.A.B xs")>]
+[ x.M(y))", "Seq.filter _.M(y)")>]
+[ x.Xs[0]) arr", "Array.map _.Xs[0] arr")>]
+[ x.M()) xs", "List.map _.M() xs")>]
+[ x.Xs.[0]) xs", "List.map _.Xs.[0] xs")>]
+[ x.M().P) xs", "List.map _.M().P xs")>]
+[ x.P ) xs", "List.map _.P xs")>]
+[ ``x``.P) xs", "List.map _.P xs")>]
+[ y.P)", "x.M(_.P)")>]
+[ x.P", "_.P")>]
+[ x.P)", "(_.P)")>]
+[
+ x.Prop) xs""",
+ "List.map _.Prop xs")>]
+let ``Lambda reading a member of its parameter converts to shorthand`` (before: string, after: string) =
+ Assert.Equal(inModule after, refactored (inModule before) "fun")
+
+[]
+[ x.P")>]
+[ x.A.B")>]
+[ x.Xs[0]")>]
+[ x.P) xs")>]
+[ x.P)")>]
+[ List.map _.M(x)", "xs |> List.map (fun x1 -> x1.M(x))")>]
+[ x.M(_.P)")>]
+let ``Shorthand converts to lambda`` (before: string, after: string) =
+ Assert.Equal(inModule after, refactored (inModule before) "_.")
+
+[]
+let ``Nested shorthand converts on its own`` () =
+ Assert.Equal(inModule "_.M(fun x -> x.P)", refactored (inModule "_.M(_.P)") "_.P")
+
+[]
+[ x.P")>]
+[ x.P")>]
+[ x.P")>]
+[ (x.P)")>]
+[ f x.P")>]
+[ x.M y")>]
+[ x.P + 1")>]
+[ x.M(x)")>]
+[ x.M(fun x -> x)")>]
+[ y.P")>]
+[ x")>]
+[ x[0]")>]
+[ 1")>]
+[ x.P) y")>]
+[ x.P @>")>]
+[]
+let ``No action`` (binding: string) =
+ let code = inModule binding
+ use context = TestContext.CreateWithCode code
+
+ let marker =
+ if binding.IndexOf("fun", StringComparison.Ordinal) >= 0 then
+ "fun"
+ else
+ "+"
+
+ Assert.Empty(actionsIn context code marker)
+
+[]
+let ``Lambda spanning multiple lines still converts to shorthand`` () =
+ let before =
+ """
+module M
+
+let r =
+ fun x ->
+ x.P
+"""
+
+ let after =
+ """
+module M
+
+let r =
+ _.P
+"""
+
+ Assert.Equal(after, refactored before "fun")
+
+[]
+let ``Converting to shorthand and back restores the lambda`` () =
+ let original = inModule "List.map (fun x -> x.P) xs"
+ let shorthand = refactored original "fun"
+ Assert.Equal(original, refactored shorthand "_.")