Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.VisualStudio/18.vNext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<inheritdoc/>` 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

Expand Down
1 change: 1 addition & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
<Compile Include="Refactor\ChangeTypeofWithNameToNameofExpression.fs" />
<Compile Include="Refactor\AddExplicitTypeToParameter.fs" />
<Compile Include="Refactor\ChangeDerefToValueRefactoring.fs" />
<Compile Include="Refactor\ConvertDotLambda.fs" />
<Compile Include="CodeFixes\IFSharpCodeFix.fs" />
<Compile Include="CodeFixes\CodeFixHelpers.fs" />
<Compile Include="CodeFixes\ChangeEqualsInFieldTypeToColon.fs" />
Expand Down
6 changes: 6 additions & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.resx
Original file line number Diff line number Diff line change
Expand Up @@ -368,4 +368,10 @@ Use live (unsaved) buffers for analysis</value>
<data name="ReturnsHeader" xml:space="preserve">
<value>Returns:</value>
</data>
<data name="ConvertToShorthandLambda" xml:space="preserve">
<value>Convert to '_.' shorthand lambda</value>
</data>
<data name="ConvertToFunLambda" xml:space="preserve">
<value>Convert to 'fun' lambda</value>
</data>
</root>
187 changes: 187 additions & 0 deletions vsintegration/src/FSharp.Editor/Refactor/ConvertDotLambda.fs
Original file line number Diff line number Diff line change
@@ -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

[<RequireQualifiedAccess>]
module private DotLambdaConversion =

[<NoComparison; NoEquality>]
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 =

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.

Is there no way to expose this logic from F.C.Service as an API ? (and reuse/parametrize/abstract with existing logic there is inside)

This duplication has potential to drift if compiler's logic changes.

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<string>(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
Comment on lines +96 to +101

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.

Extract, e.g. to a well named AP where the AP will explain what this attempts to check.

->
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)

[<ExportCodeRefactoringProvider(FSharpConstants.FSharpLanguageName, Name = "ConvertDotLambda"); Shared>]
type internal FSharpConvertDotLambdaRefactoring [<ImportingConstructor>] () =
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ module TelemetryEvents =
[<Literal>]
let CodefixActivated = "codefixactivated"

[<Literal>]
let RefactoringActivated = "refactoringactivated"

[<Literal>]
let Hints = "hints"

Expand Down
10 changes: 10 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions vsintegration/src/FSharp.Editor/xlf/FSharp.Editor.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading