From 83fd4a164708ea679bbdfe7a7e3592a646901322 Mon Sep 17 00:00:00 2001 From: Vasily Kirichenko Date: Wed, 28 Dec 2016 16:59:05 +0300 Subject: [PATCH 1/3] refactor with asyncMaybe --- .../CodeFix/ProposeUppercaseLabel.fs | 103 +++++------ .../src/FSharp.Editor/Common/Pervasive.fs | 172 +++++++++++++++++- 2 files changed, 218 insertions(+), 57 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs b/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs index 10ec21296a4..c50cccb36eb 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs @@ -36,60 +36,51 @@ type internal FSharpProposeUpperCaseLabelCodeFixProvider override __.FixableDiagnosticIds = fixableDiagnosticIds.ToImmutableArray() override __.RegisterCodeFixesAsync context : Task = - async { - if context.Span.Length > 0 then - let document = context.Document - let! sourceText = document.GetTextAsync(context.CancellationToken) - let originalText = sourceText.ToString(context.Span) - if originalText.Length > 0 then - match projectInfoManager.TryGetOptionsForEditingDocumentOrProject context.Document with - | Some options -> - let! sourceText = context.Document.GetTextAsync(context.CancellationToken) - let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) - match CommonHelpers.getSymbolAtPosition(document.Id, sourceText, context.Span.Start, document.FilePath, defines, SymbolLookupKind.Fuzzy) with - | Some symbol -> - let! textVersion = document.GetTextVersionAsync(context.CancellationToken) - let checker = checkerProvider.Checker - let! _, checkFileAnswer = checker.ParseAndCheckFileInProject(context.Document.FilePath, textVersion.GetHashCode(), sourceText.ToString(), options) - match checkFileAnswer with - | FSharpCheckFileAnswer.Aborted -> () - | FSharpCheckFileAnswer.Succeeded checkFileResults -> - let textLine = sourceText.Lines.GetLineFromPosition(context.Span.Start) - let textLinePos = sourceText.Lines.GetLinePosition(context.Span.Start) - let fcsTextLineNumber = textLinePos.Line + 1 - let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text]) - match symbolUse with - | Some symbolUse -> - match symbolUse.GetDeclarationLocation(document) with - | None -> () - | Some declLoc -> - let newText = originalText.[0].ToString().ToUpper() + originalText.Substring(1) - let title = FSComp.SR.replaceWithSuggestion newText - // defer finding all symbol uses throughout the solution until the code fix action is executed - let codeFix = - CodeAction.Create( - title, - (fun (cancellationToken: CancellationToken) -> - async { - let! symbolUsesByDocumentId = - SymbolHelpers.getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution) + asyncMaybe { + do! Option.guard (context.Span.Length > 0) + let document = context.Document + let! sourceText = document.GetTextAsync(context.CancellationToken) + let originalText = sourceText.ToString(context.Span) + do! Option.guard (originalText.Length > 0) + let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject context.Document + let! sourceText = context.Document.GetTextAsync(context.CancellationToken) + let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) + let! symbol = CommonHelpers.getSymbolAtPosition(document.Id, sourceText, context.Span.Start, document.FilePath, defines, SymbolLookupKind.Fuzzy) + let! textVersion = document.GetTextVersionAsync(context.CancellationToken) + let checker = checkerProvider.Checker + let! _, checkFileAnswer = checker.ParseAndCheckFileInProject(context.Document.FilePath, textVersion.GetHashCode(), sourceText.ToString(), options) |> liftAsync + match checkFileAnswer with + | FSharpCheckFileAnswer.Aborted -> () + | FSharpCheckFileAnswer.Succeeded checkFileResults -> + let textLine = sourceText.Lines.GetLineFromPosition(context.Span.Start) + let textLinePos = sourceText.Lines.GetLinePosition(context.Span.Start) + let fcsTextLineNumber = textLinePos.Line + 1 + let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text]) + let! declLoc = symbolUse.GetDeclarationLocation(document) + let newText = originalText.[0].ToString().ToUpper() + originalText.Substring(1) + let title = FSComp.SR.replaceWithSuggestion newText + // defer finding all symbol uses throughout the solution until the code fix action is executed + let codeFix = + CodeAction.Create( + title, + (fun (cancellationToken: CancellationToken) -> + async { + let! symbolUsesByDocumentId = + SymbolHelpers.getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution) + + let mutable solution = document.Project.Solution - let mutable solution = document.Project.Solution - - for KeyValue(documentId, symbolUses) in symbolUsesByDocumentId do - let document = document.Project.Solution.GetDocument(documentId) - let! sourceText = document.GetTextAsync(cancellationToken) - let mutable sourceText = sourceText - for symbolUse in symbolUses do - let textSpan = CommonHelpers.fixupSpan(sourceText, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate)) - sourceText <- sourceText.Replace(textSpan, newText) - solution <- solution.WithDocumentText(documentId, sourceText) - return solution - } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)), - title) - let diagnostics = (context.Diagnostics |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)).ToImmutableArray() - context.RegisterCodeFix(codeFix, diagnostics) - | None -> () - | None -> () - | _ -> () - } |> CommonRoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) \ No newline at end of file + for KeyValue(documentId, symbolUses) in symbolUsesByDocumentId do + let document = document.Project.Solution.GetDocument(documentId) + let! sourceText = document.GetTextAsync(cancellationToken) + let mutable sourceText = sourceText + for symbolUse in symbolUses do + let textSpan = CommonHelpers.fixupSpan(sourceText, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate)) + sourceText <- sourceText.Replace(textSpan, newText) + solution <- solution.WithDocumentText(documentId, sourceText) + return solution + } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)), + title) + let diagnostics = (context.Diagnostics |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)).ToImmutableArray() + context.RegisterCodeFix(codeFix, diagnostics) + } |> Async.ignore |> CommonRoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs index b90fb224958..c7643a89dee 100644 --- a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs +++ b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs @@ -1,7 +1,8 @@ [] -module Microsoft.VisualStudio.FSharp.Pervasive +module Microsoft.VisualStudio.FSharp.Editor.Pervasive open System +open System.Diagnostics [] module String = @@ -24,3 +25,172 @@ type System.IServiceProvider with member x.GetService<'T>() = x.GetService(typeof<'T>) :?> 'T member x.GetService<'S, 'T>() = x.GetService(typeof<'S>) :?> 'T +[] +type MaybeBuilder () = + // 'T -> M<'T> + [] + member inline __.Return value: 'T option = + Some value + + // M<'T> -> M<'T> + [] + member inline __.ReturnFrom value: 'T option = + value + + // unit -> M<'T> + [] + member inline __.Zero (): unit option = + Some () // TODO: Should this be None? + + // (unit -> M<'T>) -> M<'T> + [] + member __.Delay (f: unit -> 'T option): 'T option = + f () + + // M<'T> -> M<'T> -> M<'T> + // or + // M -> M<'T> -> M<'T> + [] + member inline __.Combine (r1, r2: 'T option): 'T option = + match r1 with + | None -> + None + | Some () -> + r2 + + // M<'T> * ('T -> M<'U>) -> M<'U> + [] + member inline __.Bind (value, f: 'T -> 'U option): 'U option = + Option.bind f value + + // 'T * ('T -> M<'U>) -> M<'U> when 'U :> IDisposable + [] + member __.Using (resource: ('T :> System.IDisposable), body: _ -> _ option): _ option = + try body resource + finally + if not <| obj.ReferenceEquals (null, box resource) then + resource.Dispose () + + // (unit -> bool) * M<'T> -> M<'T> + [] + member x.While (guard, body: _ option): _ option = + if guard () then + // OPTIMIZE: This could be simplified so we don't need to make calls to Bind and While. + x.Bind (body, (fun () -> x.While (guard, body))) + else + x.Zero () + + // seq<'T> * ('T -> M<'U>) -> M<'U> + // or + // seq<'T> * ('T -> M<'U>) -> seq> + [] + member x.For (sequence: seq<_>, body: 'T -> unit option): _ option = + // OPTIMIZE: This could be simplified so we don't need to make calls to Using, While, Delay. + x.Using (sequence.GetEnumerator (), fun enum -> + x.While ( + enum.MoveNext, + x.Delay (fun () -> + body enum.Current))) + +let maybe = MaybeBuilder() + +[] +type AsyncMaybeBuilder () = + [] + member __.Return value : Async<'T option> = Some value |> async.Return + + [] + member __.ReturnFrom value : Async<'T option> = value + + [] + member __.ReturnFrom (value: 'T option) : Async<'T option> = async.Return value + + [] + member __.Zero () : Async = + Some () |> async.Return + + [] + member __.Delay (f : unit -> Async<'T option>) : Async<'T option> = f () + + [] + member __.Combine (r1, r2 : Async<'T option>) : Async<'T option> = + async { + let! r1' = r1 + match r1' with + | None -> return None + | Some () -> return! r2 + } + + [] + member __.Bind (value: Async<'T option>, f : 'T -> Async<'U option>) : Async<'U option> = + async { + let! value' = value + match value' with + | None -> return None + | Some result -> return! f result + } + + [] + member __.Bind (value: System.Threading.Tasks.Task<'T>, f : 'T -> Async<'U option>) : Async<'U option> = + async { + let! value' = Async.AwaitTask value + return! f value' + } + + [] + member __.Bind (value: 'T option, f : 'T -> Async<'U option>) : Async<'U option> = + async { + match value with + | None -> return None + | Some result -> return! f result + } + + [] + member __.Using (resource : ('T :> IDisposable), body : _ -> Async<_ option>) : Async<_ option> = + try body resource + finally if not (isNull resource) then resource.Dispose () + + [] + member x.While (guard, body : Async<_ option>) : Async<_ option> = + if guard () then + x.Bind (body, (fun () -> x.While (guard, body))) + else + x.Zero () + + [] + member x.For (sequence : seq<_>, body : 'T -> Async) : Async<_ option> = + x.Using (sequence.GetEnumerator (), fun enum -> + x.While (enum.MoveNext, x.Delay (fun () -> body enum.Current))) + + [] + member inline __.TryWith (computation : Async<'T option>, catchHandler : exn -> Async<'T option>) : Async<'T option> = + async.TryWith (computation, catchHandler) + + [] + member inline __.TryFinally (computation : Async<'T option>, compensation : unit -> unit) : Async<'T option> = + async.TryFinally (computation, compensation) + +let asyncMaybe = AsyncMaybeBuilder() + +let inline liftAsync (computation : Async<'T>) : Async<'T option> = + async { + let! a = computation + return Some a + } + +module Async = + let map (f: 'T -> 'U) (a: Async<'T>) : Async<'U> = + async { + let! a = a + return f a + } + + let ignore (a: Async<'T>) : Async = + async { + let! _ = a + return () + } + +module Option = + let guard (x: bool) : Option = + if x then Some() else None \ No newline at end of file From c2720fe382235cc7624a60aaf1f69223a7a00c27 Mon Sep 17 00:00:00 2001 From: Vasily Kirichenko Date: Wed, 28 Dec 2016 17:17:08 +0300 Subject: [PATCH 2/3] add ParseAndCheckDocument --- .../CodeFix/ProposeUppercaseLabel.fs | 72 +++++++++---------- .../src/FSharp.Editor/Common/CommonHelpers.fs | 44 ++++-------- .../src/FSharp.Editor/Common/Pervasive.fs | 24 +++++++ .../src/FSharp.Editor/Common/SymbolHelpers.fs | 2 +- .../InlineRename/InlineRenameService.fs | 2 +- 5 files changed, 75 insertions(+), 69 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs b/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs index c50cccb36eb..1fa8eecde2f 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs @@ -42,45 +42,41 @@ type internal FSharpProposeUpperCaseLabelCodeFixProvider let! sourceText = document.GetTextAsync(context.CancellationToken) let originalText = sourceText.ToString(context.Span) do! Option.guard (originalText.Length > 0) - let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject context.Document - let! sourceText = context.Document.GetTextAsync(context.CancellationToken) + let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document + let! sourceText = document.GetTextAsync(context.CancellationToken) let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) let! symbol = CommonHelpers.getSymbolAtPosition(document.Id, sourceText, context.Span.Start, document.FilePath, defines, SymbolLookupKind.Fuzzy) - let! textVersion = document.GetTextVersionAsync(context.CancellationToken) let checker = checkerProvider.Checker - let! _, checkFileAnswer = checker.ParseAndCheckFileInProject(context.Document.FilePath, textVersion.GetHashCode(), sourceText.ToString(), options) |> liftAsync - match checkFileAnswer with - | FSharpCheckFileAnswer.Aborted -> () - | FSharpCheckFileAnswer.Succeeded checkFileResults -> - let textLine = sourceText.Lines.GetLineFromPosition(context.Span.Start) - let textLinePos = sourceText.Lines.GetLinePosition(context.Span.Start) - let fcsTextLineNumber = textLinePos.Line + 1 - let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text]) - let! declLoc = symbolUse.GetDeclarationLocation(document) - let newText = originalText.[0].ToString().ToUpper() + originalText.Substring(1) - let title = FSComp.SR.replaceWithSuggestion newText - // defer finding all symbol uses throughout the solution until the code fix action is executed - let codeFix = - CodeAction.Create( - title, - (fun (cancellationToken: CancellationToken) -> - async { - let! symbolUsesByDocumentId = - SymbolHelpers.getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution) - - let mutable solution = document.Project.Solution - - for KeyValue(documentId, symbolUses) in symbolUsesByDocumentId do - let document = document.Project.Solution.GetDocument(documentId) - let! sourceText = document.GetTextAsync(cancellationToken) - let mutable sourceText = sourceText - for symbolUse in symbolUses do - let textSpan = CommonHelpers.fixupSpan(sourceText, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate)) - sourceText <- sourceText.Replace(textSpan, newText) - solution <- solution.WithDocumentText(documentId, sourceText) - return solution - } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)), - title) - let diagnostics = (context.Diagnostics |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)).ToImmutableArray() - context.RegisterCodeFix(codeFix, diagnostics) + let! _, checkFileResults = checker.ParseAndCheckDocument(document, options) + let textLine = sourceText.Lines.GetLineFromPosition(context.Span.Start) + let textLinePos = sourceText.Lines.GetLinePosition(context.Span.Start) + let fcsTextLineNumber = textLinePos.Line + 1 + let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text]) + let! declLoc = symbolUse.GetDeclarationLocation(document) + let newText = originalText.[0].ToString().ToUpper() + originalText.Substring(1) + let title = FSComp.SR.replaceWithSuggestion newText + // defer finding all symbol uses throughout the solution until the code fix action is executed + let codeFix = + CodeAction.Create( + title, + (fun (cancellationToken: CancellationToken) -> + async { + let! symbolUsesByDocumentId = + SymbolHelpers.getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution) + + let mutable solution = document.Project.Solution + + for KeyValue(documentId, symbolUses) in symbolUsesByDocumentId do + let document = document.Project.Solution.GetDocument(documentId) + let! sourceText = document.GetTextAsync(cancellationToken) + let mutable sourceText = sourceText + for symbolUse in symbolUses do + let textSpan = CommonHelpers.fixupSpan(sourceText, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate)) + sourceText <- sourceText.Replace(textSpan, newText) + solution <- solution.WithDocumentText(documentId, sourceText) + return solution + } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)), + title) + let diagnostics = (context.Diagnostics |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)).ToImmutableArray() + context.RegisterCodeFix(codeFix, diagnostics) } |> Async.ignore |> CommonRoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs b/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs index 96a6be4d791..2a222d65403 100644 --- a/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs @@ -416,6 +416,20 @@ module internal Extensions = try Path.GetFullPath path with _ -> path + type FSharpChecker with + member this.ParseAndCheckDocument(document: Document, options: FSharpProjectOptions) : Async<(Ast.ParsedInput * FSharpCheckFileResults) option> = + async { + let! cancellationToken = Async.CancellationToken + let! sourceText = document.GetTextAsync() + let! textVersion = document.GetTextVersionAsync(cancellationToken) + let! parseResults, checkFileAnswer = this.ParseAndCheckFileInProject(document.FilePath, textVersion.GetHashCode(), sourceText.ToString(), options) + return + match parseResults.ParseTree, checkFileAnswer with + | _, FSharpCheckFileAnswer.Aborted + | None, _ -> None + | Some parsedInput, FSharpCheckFileAnswer.Succeeded checkResults -> Some (parsedInput, checkResults) + } + type FSharpSymbol with member this.IsInternalToProject = match this with @@ -511,32 +525,4 @@ module internal Extensions = | GlyphMajor.Error -> Glyph.Error | _ -> Glyph.None - type Async<'a> with - /// Creates an asynchronous workflow that runs the asynchronous workflow given as an argument at most once. - /// When the returned workflow is started for the second time, it reuses the result of the previous execution. - static member Cache (input : Async<'T>) = - let agent = MailboxProcessor>.Start <| fun agent -> - async { - let! replyCh = agent.Receive () - let! res = input - replyCh.Reply res - while true do - let! replyCh = agent.Receive () - replyCh.Reply res - } - async { return! agent.PostAndAsyncReply id } - - static member inline Map (f: 'a -> 'b) (input: Async<'a>) : Async<'b> = - async { - let! result = input - return f result - } - - type AsyncBuilder with - member __.Bind(computation: System.Threading.Tasks.Task<'a>, binder: 'a -> Async<'b>): Async<'b> = - async { - let! a = Async.AwaitTask computation - return! binder a - } - - member __.ReturnFrom(computation: System.Threading.Tasks.Task<'a>): Async<'a> = Async.AwaitTask computation + \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs index c7643a89dee..93254c2d10a 100644 --- a/vsintegration/src/FSharp.Editor/Common/Pervasive.fs +++ b/vsintegration/src/FSharp.Editor/Common/Pervasive.fs @@ -191,6 +191,30 @@ module Async = return () } + /// Creates an asynchronous workflow that runs the asynchronous workflow given as an argument at most once. + /// When the returned workflow is started for the second time, it reuses the result of the previous execution. + let cache (input : Async<'T>) = + let agent = MailboxProcessor>.Start <| fun agent -> + async { + let! replyCh = agent.Receive () + let! res = input + replyCh.Reply res + while true do + let! replyCh = agent.Receive () + replyCh.Reply res + } + async { return! agent.PostAndAsyncReply id } + +type AsyncBuilder with + member __.Bind(computation: System.Threading.Tasks.Task<'a>, binder: 'a -> Async<'b>): Async<'b> = + async { + let! a = Async.AwaitTask computation + return! binder a + } + + member __.ReturnFrom(computation: System.Threading.Tasks.Task<'a>): Async<'a> = Async.AwaitTask computation + + module Option = let guard (x: bool) : Option = if x then Some() else None \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs index 665ed8749ac..7fb7bd55dbe 100644 --- a/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs @@ -46,7 +46,7 @@ module internal SymbolHelpers = | None -> return [||] }) |> Async.Parallel - |> Async.Map Array.concat + |> Async.map Array.concat return (symbolUses diff --git a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs index 272cbd0ee3f..297d2b9400f 100644 --- a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs +++ b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs @@ -93,7 +93,7 @@ type internal InlineRenameInfo let symbolUses = SymbolHelpers.getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution) - |> Async.Cache + |> Async.cache interface IInlineRenameInfo with member __.CanRename = true From c06642b2686bb72b7808e728b656977987a277f6 Mon Sep 17 00:00:00 2001 From: Vasily Kirichenko Date: Wed, 28 Dec 2016 17:50:03 +0300 Subject: [PATCH 3/3] refactor ProposeUppercaseLabel --- .../CodeFix/ProposeUppercaseLabel.fs | 63 ++----------------- .../Common/CommonRoslynHelpers.fs | 2 +- .../src/FSharp.Editor/Common/SymbolHelpers.fs | 40 ++++++++++++ 3 files changed, 47 insertions(+), 58 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs b/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs index 1fa8eecde2f..1483dd8e0f1 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs @@ -4,24 +4,9 @@ namespace rec Microsoft.VisualStudio.FSharp.Editor open System.Composition open System.Collections.Immutable -open System.Threading open System.Threading.Tasks - -open Microsoft.CodeAnalysis -open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.CodeFixes open Microsoft.CodeAnalysis.CodeActions -open Microsoft.VisualStudio.FSharp.LanguageService -open Microsoft.VisualStudio.Text.Tagging -open Microsoft.VisualStudio.Text.Formatting -open Microsoft.VisualStudio.Shell.Interop - -open Microsoft.FSharp.Compiler -open Microsoft.FSharp.Compiler.Parser -open Microsoft.FSharp.Compiler.Range -open Microsoft.FSharp.Compiler.SourceCodeServices - -open System.Windows.Documents [] type internal FSharpProposeUpperCaseLabelCodeFixProvider @@ -37,46 +22,10 @@ type internal FSharpProposeUpperCaseLabelCodeFixProvider override __.RegisterCodeFixesAsync context : Task = asyncMaybe { - do! Option.guard (context.Span.Length > 0) - let document = context.Document - let! sourceText = document.GetTextAsync(context.CancellationToken) - let originalText = sourceText.ToString(context.Span) - do! Option.guard (originalText.Length > 0) - let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document - let! sourceText = document.GetTextAsync(context.CancellationToken) - let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) - let! symbol = CommonHelpers.getSymbolAtPosition(document.Id, sourceText, context.Span.Start, document.FilePath, defines, SymbolLookupKind.Fuzzy) - let checker = checkerProvider.Checker - let! _, checkFileResults = checker.ParseAndCheckDocument(document, options) - let textLine = sourceText.Lines.GetLineFromPosition(context.Span.Start) - let textLinePos = sourceText.Lines.GetLinePosition(context.Span.Start) - let fcsTextLineNumber = textLinePos.Line + 1 - let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text]) - let! declLoc = symbolUse.GetDeclarationLocation(document) - let newText = originalText.[0].ToString().ToUpper() + originalText.Substring(1) - let title = FSComp.SR.replaceWithSuggestion newText - // defer finding all symbol uses throughout the solution until the code fix action is executed - let codeFix = - CodeAction.Create( - title, - (fun (cancellationToken: CancellationToken) -> - async { - let! symbolUsesByDocumentId = - SymbolHelpers.getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution) - - let mutable solution = document.Project.Solution - - for KeyValue(documentId, symbolUses) in symbolUsesByDocumentId do - let document = document.Project.Solution.GetDocument(documentId) - let! sourceText = document.GetTextAsync(cancellationToken) - let mutable sourceText = sourceText - for symbolUse in symbolUses do - let textSpan = CommonHelpers.fixupSpan(sourceText, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate)) - sourceText <- sourceText.Replace(textSpan, newText) - solution <- solution.WithDocumentText(documentId, sourceText) - return solution - } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)), - title) - let diagnostics = (context.Diagnostics |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)).ToImmutableArray() - context.RegisterCodeFix(codeFix, diagnostics) + let textChanger (originalText: string) = originalText.[0].ToString().ToUpper() + originalText.Substring(1) + let! solutionChanger, originalText = SymbolHelpers.changeAllSymbolReferences(context.Document, context.Span, textChanger, projectInfoManager, checkerProvider.Checker) + let title = FSComp.SR.replaceWithSuggestion (textChanger originalText) + context.RegisterCodeFix( + CodeAction.Create(title, solutionChanger, title), + (context.Diagnostics |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)).ToImmutableArray()) } |> Async.ignore |> CommonRoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Common/CommonRoslynHelpers.fs b/vsintegration/src/FSharp.Editor/Common/CommonRoslynHelpers.fs index 48c243d3ccf..367ac018ccf 100644 --- a/vsintegration/src/FSharp.Editor/Common/CommonRoslynHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/CommonRoslynHelpers.fs @@ -94,4 +94,4 @@ module internal RoslynExtensions = member this.GetDependentProjects() = [ for project in this.Solution.Projects do if project.ProjectReferences |> Seq.exists (fun ref -> ref.ProjectId = this.Id) then - yield project ] + yield project ] \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs index 7fb7bd55dbe..c166a32201b 100644 --- a/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs @@ -58,4 +58,44 @@ module internal SymbolHelpers = fun (_, xs) -> xs |> Seq.map snd |> Seq.toArray) } + type OriginalText = string + let changeAllSymbolReferences (document: Document, symbolSpan: TextSpan, textChanger: string -> string, projectInfoManager: ProjectInfoManager, checker: FSharpChecker) + : Async<(Func> * OriginalText) option> = + asyncMaybe { + do! Option.guard (symbolSpan.Length > 0) + let! cancellationToken = liftAsync Async.CancellationToken + let! sourceText = document.GetTextAsync(cancellationToken) + let originalText = sourceText.ToString(symbolSpan) + do! Option.guard (originalText.Length > 0) + let! options = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document + let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) + let! symbol = CommonHelpers.getSymbolAtPosition(document.Id, sourceText, symbolSpan.Start, document.FilePath, defines, SymbolLookupKind.Fuzzy) + let! _, checkFileResults = checker.ParseAndCheckDocument(document, options) + let textLine = sourceText.Lines.GetLineFromPosition(symbolSpan.Start) + let textLinePos = sourceText.Lines.GetLinePosition(symbolSpan.Start) + let fcsTextLineNumber = textLinePos.Line + 1 + let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text]) + let! declLoc = symbolUse.GetDeclarationLocation(document) + let newText = textChanger originalText + // defer finding all symbol uses throughout the solution + return + Func<_,_>(fun (cancellationToken: CancellationToken) -> + async { + let! symbolUsesByDocumentId = + getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution) + + let mutable solution = document.Project.Solution + + for KeyValue(documentId, symbolUses) in symbolUsesByDocumentId do + let document = document.Project.Solution.GetDocument(documentId) + let! sourceText = document.GetTextAsync(cancellationToken) + let mutable sourceText = sourceText + for symbolUse in symbolUses do + let textSpan = CommonHelpers.fixupSpan(sourceText, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate)) + sourceText <- sourceText.Replace(textSpan, newText) + solution <- solution.WithDocumentText(documentId, sourceText) + return solution + } |> CommonRoslynHelpers.StartAsyncAsTask cancellationToken), + originalText + }