Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Democratizing code fixes - part three #15508

Merged
merged 16 commits into from
Jun 30, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ type internal AddInstanceMemberParameterCodeFixProvider() =
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this)

interface IFSharpCodeFixProvider with
member _.GetCodeFixIfAppliesAsync _ span =
member _.GetCodeFixIfAppliesAsync context =
let codeFix =
{
Name = CodeFix.AddInstanceMemberParameter
Message = title
Changes = [ TextChange(TextSpan(span.Start, 0), "x.") ]
Changes = [ TextChange(TextSpan(context.Span.Start, 0), "x.") ]
}

CancellableTask.singleton (Some codeFix)
95 changes: 53 additions & 42 deletions vsintegration/src/FSharp.Editor/CodeFixes/AddMissingFunKeyword.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,63 @@ open System.Collections.Immutable
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.CodeFixes

open CancellableTasks

[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.AddMissingFunKeyword); Shared>]
type internal AddMissingFunKeywordCodeFixProvider [<ImportingConstructor>] () =
inherit CodeFixProvider()

static let title = SR.AddMissingFunKeyword()

let adjustPosition (sourceText: SourceText) (span: TextSpan) =
let rec loop ch pos =
if not (Char.IsWhiteSpace(ch)) then
pos
else
loop sourceText.[pos] (pos - 1)

loop (sourceText.[span.Start - 1]) span.Start
psfinaki marked this conversation as resolved.
Show resolved Hide resolved

override _.FixableDiagnosticIds = ImmutableArray.Create("FS0010")

override _.RegisterCodeFixesAsync context =
asyncMaybe {
let document = context.Document
let! sourceText = context.Document.GetTextAsync(context.CancellationToken)
let textOfError = sourceText.GetSubText(context.Span).ToString()

// Only trigger when failing to parse `->`, which arises when `fun` is missing
do! Option.guard (textOfError = "->")

let! defines, langVersion =
document.GetFSharpCompilationDefinesAndLangVersionAsync(nameof (AddMissingFunKeywordCodeFixProvider))
|> liftAsync

let adjustedPosition =
let rec loop ch pos =
if not (Char.IsWhiteSpace(ch)) then
pos
else
loop sourceText.[pos] (pos - 1)

loop sourceText.[context.Span.Start - 1] context.Span.Start

let! intendedArgLexerSymbol =
Tokenizer.getSymbolAtPosition (
document.Id,
sourceText,
adjustedPosition,
document.FilePath,
defines,
SymbolLookupKind.Greedy,
false,
false,
Some langVersion,
context.CancellationToken
)

let! intendedArgSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, intendedArgLexerSymbol.Range)

do context.RegisterFsharpFix(CodeFix.AddMissingFunKeyword, title, [| TextChange(TextSpan(intendedArgSpan.Start, 0), "fun ") |])
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this)

interface IFSharpCodeFixProvider with
member _.GetCodeFixIfAppliesAsync context =
cancellableTask {
let! textOfError = context.GetSquigglyTextAsync()

if textOfError <> "->" then
return None
else
let! cancellationToken = CancellableTask.getCurrentCancellationToken ()
let document = context.Document

let! defines, langVersion =
document.GetFSharpCompilationDefinesAndLangVersionAsync(nameof (AddMissingFunKeywordCodeFixProvider))

let! sourceText = context.GetSourceTextAsync()
let adjustedPosition = adjustPosition sourceText context.Span

return
Tokenizer.getSymbolAtPosition (
document.Id,
sourceText,
adjustedPosition,
document.FilePath,
defines,
SymbolLookupKind.Greedy,
false,
false,
Some langVersion,
cancellationToken
)
|> Option.bind (fun intendedArgLexerSymbol ->
RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, intendedArgLexerSymbol.Range))
|> Option.map (fun intendedArgSpan ->
{
Name = CodeFix.AddMissingFunKeyword
Message = title
Changes = [ TextChange(TextSpan(intendedArgSpan.Start, 0), "fun ") ]
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ type internal AddMissingRecToMutuallyRecFunctionsCodeFixProvider [<ImportingCons
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this)

interface IFSharpCodeFixProvider with
member _.GetCodeFixIfAppliesAsync document span =
member _.GetCodeFixIfAppliesAsync context =
cancellableTask {
let! cancellationToken = CancellableTask.getCurrentCancellationToken ()

let! defines, langVersion =
document.GetFSharpCompilationDefinesAndLangVersionAsync(nameof (AddMissingRecToMutuallyRecFunctionsCodeFixProvider))
context.Document.GetFSharpCompilationDefinesAndLangVersionAsync(
nameof (AddMissingRecToMutuallyRecFunctionsCodeFixProvider)
)

let! sourceText = document.GetTextAsync(cancellationToken)
let! sourceText = context.GetSourceTextAsync()

let funcStartPos =
let rec loop ch pos =
Expand All @@ -38,14 +40,14 @@ type internal AddMissingRecToMutuallyRecFunctionsCodeFixProvider [<ImportingCons
else
loop sourceText.[pos + 1] (pos + 1)

loop sourceText.[span.End + 1] (span.End + 1)
loop sourceText.[context.Span.End + 1] (context.Span.End + 1)

return
Tokenizer.getSymbolAtPosition (
document.Id,
context.Document.Id,
sourceText,
funcStartPos,
document.FilePath,
context.Document.FilePath,
defines,
SymbolLookupKind.Greedy,
false,
Expand All @@ -59,6 +61,6 @@ type internal AddMissingRecToMutuallyRecFunctionsCodeFixProvider [<ImportingCons
{
Name = CodeFix.AddMissingRecToMutuallyRecFunctions
Message = String.Format(titleFormat, funcName)
Changes = [ TextChange(TextSpan(span.End, 0), " rec") ]
Changes = [ TextChange(TextSpan(context.Span.End, 0), " rec") ]
})
}
9 changes: 3 additions & 6 deletions vsintegration/src/FSharp.Editor/CodeFixes/ChangeToUpcast.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ type internal ChangeToUpcastCodeFixProvider() =
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this)

interface IFSharpCodeFixProvider with
member _.GetCodeFixIfAppliesAsync document span =
member _.GetCodeFixIfAppliesAsync context =
cancellableTask {
let! cancellationToken = CancellableTask.getCurrentCancellationToken ()

let! sourceText = document.GetTextAsync(cancellationToken)
let text = sourceText.GetSubText(span).ToString()
let! text = context.GetSquigglyTextAsync()

// Only works if it's one or the other
let isDowncastOperator = text.Contains(":?>")
Expand All @@ -40,7 +37,7 @@ type internal ChangeToUpcastCodeFixProvider() =
else
text.Replace("downcast", "upcast")

let changes = [ TextChange(span, replacement) ]
let changes = [ TextChange(context.Span, replacement) ]

let title =
if isDowncastOperator then
Expand Down
20 changes: 19 additions & 1 deletion vsintegration/src/FSharp.Editor/CodeFixes/CodeFixHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,26 @@ module internal CodeFixExtensions =

member ctx.RegisterFsharpFix(codeFix: IFSharpCodeFixProvider) =
cancellableTask {
match! codeFix.GetCodeFixIfAppliesAsync ctx.Document ctx.Span with
match! codeFix.GetCodeFixIfAppliesAsync ctx with
| Some codeFix -> ctx.RegisterFsharpFix(codeFix.Name, codeFix.Message, codeFix.Changes)
| None -> ()
}
|> CancellableTask.startAsTask ctx.CancellationToken

member ctx.GetSourceTextAsync() =
cancellableTask {
let! cancellationToken = CancellableTask.getCurrentCancellationToken ()
return! ctx.Document.GetTextAsync cancellationToken
}

member ctx.GetSquigglyTextAsync() =
cancellableTask {
let! sourceText = ctx.GetSourceTextAsync()
return sourceText.GetSubText(ctx.Span).ToString()
}

member ctx.GetErrorRangeAsync() =
cancellableTask {
let! sourceText = ctx.GetSourceTextAsync()
return RoslynHelpers.TextSpanToFSharpRange(ctx.Document.FilePath, ctx.Span, sourceText)
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,26 @@ type internal ConvertCSharpLambdaToFSharpLambdaCodeFixProvider [<ImportingConstr
RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, lambdaBodyRange))
|> flatten3

override _.FixableDiagnosticIds = ImmutableArray.Create("FS0039")
override _.FixableDiagnosticIds = ImmutableArray.Create "FS0039"

override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this)
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix this

interface IFSharpCodeFixProvider with
member _.GetCodeFixIfAppliesAsync document span =
member _.GetCodeFixIfAppliesAsync context =
cancellableTask {
let! cancellationToken = CancellableTask.getCurrentCancellationToken ()

let! parseResults = document.GetFSharpParseResultsAsync(nameof (ConvertCSharpLambdaToFSharpLambdaCodeFixProvider))

let! sourceText = document.GetTextAsync(cancellationToken)

let errorRange =
RoslynHelpers.TextSpanToFSharpRange(document.FilePath, span, sourceText)
let! parseResults = context.Document.GetFSharpParseResultsAsync(nameof ConvertCSharpLambdaToFSharpLambdaCodeFixProvider)
let! sourceText = context.Document.GetTextAsync(cancellationToken)
let! errorRange = context.GetErrorRangeAsync()

return
tryGetSpans parseResults errorRange sourceText
|> Option.map (fun (fullParenSpan, lambdaArgSpan, lambdaBodySpan) ->
let replacement =
let argText = sourceText.GetSubText(lambdaArgSpan).ToString()
let bodyText = sourceText.GetSubText(lambdaBodySpan).ToString()
TextChange(fullParenSpan, "fun " + argText + " -> " + bodyText)
TextChange(fullParenSpan, $"fun {argText} -> {bodyText}")

{
Name = CodeFix.ConvertCSharpLambdaToFSharpLambda
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,11 @@ type internal ConvertToAnonymousRecordCodeFixProvider [<ImportingConstructor>] (
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this)

interface IFSharpCodeFixProvider with
member _.GetCodeFixIfAppliesAsync document span =
member _.GetCodeFixIfAppliesAsync context =
cancellableTask {
let! cancellationToken = CancellableTask.getCurrentCancellationToken ()

let! parseResults = document.GetFSharpParseResultsAsync(nameof (ConvertToAnonymousRecordCodeFixProvider))

let! sourceText = document.GetTextAsync(cancellationToken)

let errorRange =
RoslynHelpers.TextSpanToFSharpRange(document.FilePath, span, sourceText)
let! parseResults = context.Document.GetFSharpParseResultsAsync(nameof ConvertToAnonymousRecordCodeFixProvider)
let! sourceText = context.GetSourceTextAsync()
let! errorRange = context.GetErrorRangeAsync()

return
parseResults.TryRangeOfRecordExpressionContainingPos errorRange.Start
Expand Down
4 changes: 2 additions & 2 deletions vsintegration/src/FSharp.Editor/CodeFixes/IFSharpCodeFix.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Microsoft.VisualStudio.FSharp.Editor

open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.CodeFixes
open Microsoft.CodeAnalysis.Text

open CancellableTasks
Expand All @@ -15,4 +15,4 @@ type FSharpCodeFix =
}

type IFSharpCodeFixProvider =
abstract member GetCodeFixIfAppliesAsync: document: Document -> span: TextSpan -> CancellableTask<FSharpCodeFix option>
abstract member GetCodeFixIfAppliesAsync: context: CodeFixContext -> CancellableTask<FSharpCodeFix option>
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,40 @@ open System.Collections.Immutable
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.CodeFixes

open CancellableTasks

[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.MakeOuterBindingRecursive); Shared>]
type internal MakeOuterBindingRecursiveCodeFixProvider [<ImportingConstructor>] () =
inherit CodeFixProvider()

override _.FixableDiagnosticIds = ImmutableArray.Create("FS0039")

override _.RegisterCodeFixesAsync context =
asyncMaybe {
let! parseResults =
context.Document.GetFSharpParseResultsAsync(nameof (MakeOuterBindingRecursiveCodeFixProvider))
|> liftAsync

let! sourceText = context.Document.GetTextAsync(context.CancellationToken)

let diagnosticRange =
RoslynHelpers.TextSpanToFSharpRange(context.Document.FilePath, context.Span, sourceText)

do! Option.guard (parseResults.IsPosContainedInApplication diagnosticRange.Start)

let! outerBindingRange = parseResults.TryRangeOfNameOfNearestOuterBindingContainingPos diagnosticRange.Start
let! outerBindingNameSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, outerBindingRange)

// One last check to verify the names are the same
do!
Option.guard (
sourceText
.GetSubText(outerBindingNameSpan)
.ContentEquals(sourceText.GetSubText(context.Span))
)

let title =
String.Format(SR.MakeOuterBindingRecursive(), sourceText.GetSubText(outerBindingNameSpan).ToString())

do
context.RegisterFsharpFix(
CodeFix.MakeOuterBindingRecursive,
title,
[| TextChange(TextSpan(outerBindingNameSpan.Start, 0), "rec ") |]
)
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
override this.RegisterCodeFixesAsync context = context.RegisterFsharpFix(this)

interface IFSharpCodeFixProvider with
member _.GetCodeFixIfAppliesAsync context =
cancellableTask {
let! parseResults = context.Document.GetFSharpParseResultsAsync(nameof MakeOuterBindingRecursiveCodeFixProvider)
let! sourceText = context.GetSourceTextAsync()
let! diagnosticRange = context.GetErrorRangeAsync()

if not <| parseResults.IsPosContainedInApplication diagnosticRange.Start then
return None
else
return
parseResults.TryRangeOfNameOfNearestOuterBindingContainingPos diagnosticRange.Start
|> Option.bind (fun bindingRange -> RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, bindingRange))
|> Option.filter (fun bindingSpan ->
sourceText
.GetSubText(bindingSpan)
.ContentEquals(sourceText.GetSubText context.Span))
|> Option.map (fun bindingSpan ->
let title =
String.Format(SR.MakeOuterBindingRecursive(), sourceText.GetSubText(bindingSpan).ToString())

{
Name = CodeFix.MakeOuterBindingRecursive
Message = title
Changes = [ TextChange(TextSpan(bindingSpan.Start, 0), "rec ") ]
})
}