Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,13 @@ type internal FSharpAddInstanceMemberParameterCodeFixProvider() =
inherit CodeFixProvider()

let fixableDiagnosticIds = set [ "FS0673" ]
static let title = SR.AddMissingInstanceMemberParameter()

override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds

override _.RegisterCodeFixesAsync context : Task =
asyncMaybe {
let diagnostics =
context.Diagnostics
|> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id)
|> Seq.toImmutableArray

let title = SR.AddMissingInstanceMemberParameter()

let codeFix =
CodeFixHelpers.createTextChangeCodeFix (
CodeFix.AddInstanceMemberParameter,
title,
context,
(fun () -> asyncMaybe.Return [| TextChange(TextSpan(context.Span.Start, 0), "x.") |])
)

context.RegisterCodeFix(codeFix, diagnostics)
do context.RegisterFsharpFix(CodeFix.AddInstanceMemberParameter, title, [| TextChange(TextSpan(context.Span.Start, 0), "x.") |])
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,11 @@ type internal FSharpAddMissingEqualsToTypeDefinitionCodeFixProvider() =
inherit CodeFixProvider()

let fixableDiagnosticIds = set [ "FS3360" ]

static let title = SR.AddMissingEqualsToTypeDefinition()
override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds

override _.RegisterCodeFixesAsync context : Task =
asyncMaybe {
let diagnostics =
context.Diagnostics
|> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id)
|> Seq.toImmutableArray

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

Expand All @@ -37,19 +33,14 @@ type internal FSharpAddMissingEqualsToTypeDefinitionCodeFixProvider() =
pos <- pos - 1
ch <- sourceText.[pos]

let title = SR.AddMissingEqualsToTypeDefinition()

let codeFix =
CodeFixHelpers.createTextChangeCodeFix (
do
context.RegisterFsharpFix(
CodeFix.AddMissingEqualsToTypeDefinition,
title,
context,
// 'pos + 1' is here because 'pos' is now the position of the first non-whitespace character.
// Using just 'pos' will creat uncompilable code.
(fun () -> asyncMaybe.Return [| TextChange(TextSpan(pos + 1, 0), " =") |])
[| TextChange(TextSpan(pos + 1, 0), " =") |]
)

context.RegisterCodeFix(codeFix, diagnostics)
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
18 changes: 2 additions & 16 deletions vsintegration/src/FSharp.Editor/CodeFix/AddMissingFunKeyword.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ open FSharp.Compiler.CodeAnalysis
[<ExportCodeFixProvider(FSharpConstants.FSharpLanguageName, Name = CodeFix.AddMissingFunKeyword); Shared>]
type internal FSharpAddMissingFunKeywordCodeFixProvider [<ImportingConstructor>] () =
inherit CodeFixProvider()
static let title = SR.AddMissingFunKeyword()

let fixableDiagnosticIds = set [ "FS0010" ]

Expand Down Expand Up @@ -56,22 +57,7 @@ type internal FSharpAddMissingFunKeywordCodeFixProvider [<ImportingConstructor>]

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

let diagnostics =
context.Diagnostics
|> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id)
|> Seq.toImmutableArray

let title = SR.AddMissingFunKeyword()

let codeFix =
CodeFixHelpers.createTextChangeCodeFix (
CodeFix.AddMissingFunKeyword,
title,
context,
(fun () -> asyncMaybe.Return [| TextChange(TextSpan(intendedArgSpan.Start, 0), "fun ") |])
)

context.RegisterCodeFix(codeFix, diagnostics)
do context.RegisterFsharpFix(CodeFix.AddMissingFunKeyword, title, [| TextChange(TextSpan(intendedArgSpan.Start, 0), "fun ") |])
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,37 @@
namespace Microsoft.VisualStudio.FSharp.Editor

open System.Composition
open System.Threading
open System.Threading.Tasks
open System.Collections.Immutable

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

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

static let fixableDiagnosticIds = set [ "FS0760" ]
static let title = SR.AddNewKeyword()
override _.FixableDiagnosticIds = ImmutableArray.Create "FS0760"

override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds
member this.GetChanges(_document: Document, diagnostics: ImmutableArray<Diagnostic>, _ct: CancellationToken) =
backgroundTask {

override _.RegisterCodeFixesAsync context : Task =
async {
let title = SR.AddNewKeyword()
let changes =
diagnostics
|> Seq.map (fun d -> TextChange(TextSpan(d.Location.SourceSpan.Start, 0), "new "))

let diagnostics =
context.Diagnostics
|> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id)
|> Seq.toImmutableArray

let codeFix =
CodeFixHelpers.createTextChangeCodeFix (
CodeFix.AddNewKeyword,
title,
context,
(fun () -> asyncMaybe.Return [| TextChange(TextSpan(context.Span.Start, 0), "new ") |])
)
return changes
}

context.RegisterCodeFix(codeFix, diagnostics)
override this.RegisterCodeFixesAsync ctx : Task =
backgroundTask {
let! changes = this.GetChanges(ctx.Document, ctx.Diagnostics, ctx.CancellationToken)
ctx.RegisterFsharpFix(CodeFix.AddNewKeyword, title, changes)
}
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)

override this.GetFixAllProvider() =
CodeFixHelpers.createFixAllProvider CodeFix.AddNewKeyword this.GetChanges
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type internal FSharpAddOpenCodeFixProvider [<ImportingConstructor>] (assemblyCon
CodeFix.AddOpen,
fixUnderscoresInMenuText fullName,
context,
(fun () -> asyncMaybe.Return [| TextChange(context.Span, qualifier) |])
[| TextChange(context.Span, qualifier) |]
)

let openNamespaceFix (context: CodeFixContext) ctx name ns multipleNames =
Expand Down Expand Up @@ -77,12 +77,7 @@ type internal FSharpAddOpenCodeFixProvider [<ImportingConstructor>] (assemblyCon
|> Seq.toList

for codeFix in openNamespaceFixes @ qualifiedSymbolFixes do
context.RegisterCodeFix(
codeFix,
context.Diagnostics
|> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)
|> Seq.toImmutableArray
)
context.RegisterCodeFix(codeFix, context.Diagnostics)

override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,12 @@ type internal FSharpChangePrefixNegationToInfixSubtractionodeFixProvider() =
inherit CodeFixProvider()

let fixableDiagnosticIds = set [ "FS0003" ]
static let title = SR.ChangePrefixNegationToInfixSubtraction()

override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds

override _.RegisterCodeFixesAsync context : Task =
asyncMaybe {
let diagnostics =
context.Diagnostics
|> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id)
|> Seq.toImmutableArray

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

let mutable pos = context.Span.End + 1
Expand All @@ -39,18 +35,7 @@ type internal FSharpChangePrefixNegationToInfixSubtractionodeFixProvider() =

// Bail if this isn't a negation
do! Option.guard (ch = '-')

let title = SR.ChangePrefixNegationToInfixSubtraction()

let codeFix =
CodeFixHelpers.createTextChangeCodeFix (
CodeFix.ChangePrefixNegationToInfixSubtraction,
title,
context,
(fun () -> asyncMaybe.Return [| TextChange(TextSpan(pos, 1), "- ") |])
)

context.RegisterCodeFix(codeFix, diagnostics)
do context.RegisterFsharpFix(CodeFix.ChangePrefixNegationToInfixSubtraction, title, [| TextChange(TextSpan(pos, 1), "- ") |])
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type internal FSharpChangeRefCellDerefToNotExpressionCodeFixProvider [<Importing
inherit CodeFixProvider()

let fixableDiagnosticIds = set [ "FS0001" ]
static let title = SR.UseNotForNegation()

override _.FixableDiagnosticIds = Seq.toImmutableArray fixableDiagnosticIds

Expand All @@ -32,22 +33,7 @@ type internal FSharpChangeRefCellDerefToNotExpressionCodeFixProvider [<Importing
let! derefRange = parseResults.TryRangeOfRefCellDereferenceContainingPos errorRange.Start
let! derefSpan = RoslynHelpers.TryFSharpRangeToTextSpan(sourceText, derefRange)

let title = SR.UseNotForNegation()

let diagnostics =
context.Diagnostics
|> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id)
|> Seq.toImmutableArray

let codeFix =
CodeFixHelpers.createTextChangeCodeFix (
CodeFix.ChangeRefCellDerefToNotExpression,
title,
context,
(fun () -> asyncMaybe.Return [| TextChange(derefSpan, "not ") |])
)

context.RegisterCodeFix(codeFix, diagnostics)
do context.RegisterFsharpFix(CodeFix.ChangeRefCellDerefToNotExpression, title, [| TextChange(derefSpan, "not ") |])
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
15 changes: 1 addition & 14 deletions vsintegration/src/FSharp.Editor/CodeFix/ChangeToUpcast.fs
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,7 @@ type internal FSharpChangeToUpcastCodeFixProvider() =
else
SR.UseUpcastKeyword()

let diagnostics =
context.Diagnostics
|> Seq.filter (fun x -> fixableDiagnosticIds |> Set.contains x.Id)
|> Seq.toImmutableArray

let codeFix =
CodeFixHelpers.createTextChangeCodeFix (
CodeFix.ChangeToUpcast,
title,
context,
(fun () -> asyncMaybe.Return [| TextChange(context.Span, replacement) |])
)

context.RegisterCodeFix(codeFix, diagnostics)
do context.RegisterFsharpFix(CodeFix.ChangeToUpcast, title, [| TextChange(context.Span, replacement) |])
}
|> Async.Ignore
|> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken)
93 changes: 48 additions & 45 deletions vsintegration/src/FSharp.Editor/CodeFix/CodeFixHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,75 @@

namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Threading
open System.Threading.Tasks
open System.Collections.Immutable
open System.Diagnostics

open Microsoft
open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.CodeFixes
open Microsoft.CodeAnalysis.CodeActions
open Microsoft.VisualStudio.FSharp.Editor.Telemetry

[<RequireQualifiedAccess>]
module internal CodeFixHelpers =
let createTextChangeCodeFix
(
name: string,
title: string,
context: CodeFixContext,
computeTextChanges: unit -> Async<TextChange[] option>
) =

// Currently there should be one Id here.
// Keeping it this way to be error- and futureproof
// as the underlying API does allow multiple Ids here.
let ids = context.Diagnostics |> Seq.map (fun d -> d.Id) |> String.concat ","
let private reportCodeFixTelemetry (diagnostics: ImmutableArray<Diagnostic>) (doc: Document) (staticName: string) (additionalProps) =
let ids =
diagnostics |> Seq.map (fun d -> d.Id) |> Seq.distinct |> String.concat ","

let props: (string * obj) list =
[
"name", name
additionalProps
@ [
"name", staticName
"ids", ids

// The following can help building a unique but anonymized codefix target:
// #projectid#documentid#span
// Then we can check if the codefix was actually activated after its creation.
"context.document.project.id", context.Document.Project.Id.Id.ToString()
"context.document.id", context.Document.Id.Id.ToString()
"context.span", context.Span.ToString()
"context.document.project.id", doc.Project.Id.Id.ToString()
"context.document.id", doc.Id.Id.ToString()
"context.diagnostics.count", diagnostics.Length
]

TelemetryReporter.reportEvent "codefixregistered" props
TelemetryReporter.reportEvent "codefixactivated" props

let createFixAllProvider name getChanges =
FixAllProvider.Create(fun fixAllCtx doc allDiagnostics ->
backgroundTask {
let sw = Stopwatch.StartNew()
let! (changes: seq<TextChange>) = getChanges (doc, allDiagnostics, fixAllCtx.CancellationToken)
let! text = doc.GetTextAsync(fixAllCtx.CancellationToken)
let doc = doc.WithText(text.WithChanges(changes))

do
reportCodeFixTelemetry
allDiagnostics
doc
name
[ "scope", fixAllCtx.Scope.ToString(); "elapsedMs", sw.ElapsedMilliseconds ]

return doc
})

let createTextChangeCodeFix (name: string, title: string, context: CodeFixContext, changes: TextChange seq) =
CodeAction.Create(
title,
(fun (cancellationToken: CancellationToken) ->
async {
let! sourceText = context.Document.GetTextAsync(cancellationToken) |> Async.AwaitTask
let! changesOpt = computeTextChanges ()

match changesOpt with
| None -> return context.Document
| Some textChanges ->
// Note: "activated" doesn't mean "applied".
// It's one step prior to that:
// e.g. when one clicks (Ctrl + .) and looks at the potential change.
TelemetryReporter.reportEvent "codefixactivated" props
return context.Document.WithText(sourceText.WithChanges(textChanges))
}
|> RoslynHelpers.StartAsyncAsTask(cancellationToken)),
title
backgroundTask {
let! sourceText = context.Document.GetTextAsync(cancellationToken)
let doc = context.Document.WithText(sourceText.WithChanges(changes))
reportCodeFixTelemetry context.Diagnostics context.Document name []
return doc
}),
name
)

[<AutoOpen>]
module internal CodeFixExtensions =
type CodeFixProvider with

member this.GetPrunedDiagnostics(context: CodeFixContext) =
context.Diagnostics.RemoveAll(fun x -> this.FixableDiagnosticIds.Contains(x.Id) |> not)
type CodeFixContext with

member this.RegisterFix(name, title, context: CodeFixContext, fixChange) =
let replaceCodeFix =
CodeFixHelpers.createTextChangeCodeFix (name, title, context, (fun () -> asyncMaybe.Return [| fixChange |]))
member ctx.RegisterFsharpFix(staticName, title, changes, ?diagnostics) =
let codeAction =
CodeFixHelpers.createTextChangeCodeFix (staticName, title, ctx, changes)

context.RegisterCodeFix(replaceCodeFix, this.GetPrunedDiagnostics(context))
let diag = diagnostics |> Option.defaultValue ctx.Diagnostics
ctx.RegisterCodeFix(codeAction, diag)
Loading