diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs b/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs index 6ebe6a5351e..10ec21296a4 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ProposeUppercaseLabel.fs @@ -11,31 +11,85 @@ 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() = +type internal FSharpProposeUpperCaseLabelCodeFixProvider + [] + ( + checkerProvider: FSharpCheckerProvider, + projectInfoManager: ProjectInfoManager + ) = inherit CodeFixProvider() let fixableDiagnosticIds = ["FS0053"] - let createCodeFix (title: string, context: CodeFixContext, textChange: TextChange) = - CodeAction.Create( - title, - (fun (cancellationToken: CancellationToken) -> - async { - let! sourceText = context.Document.GetTextAsync() - return context.Document.WithText(sourceText.WithChanges(textChange)) - } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)), - title) - override __.FixableDiagnosticIds = fixableDiagnosticIds.ToImmutableArray() override __.RegisterCodeFixesAsync context : Task = async { - let diagnostics = (context.Diagnostics |> Seq.filter (fun x -> fixableDiagnosticIds |> List.contains x.Id)).ToImmutableArray() if context.Span.Length > 0 then - let! sourceText = context.Document.GetTextAsync(context.CancellationToken) + let document = context.Document + let! sourceText = document.GetTextAsync(context.CancellationToken) let originalText = sourceText.ToString(context.Span) if originalText.Length > 0 then - let newText = originalText.[0].ToString().ToUpper() + originalText.Substring(1) - context.RegisterCodeFix(createCodeFix(FSComp.SR.replaceWithSuggestion newText, context, TextChange(context.Span, newText)), diagnostics) + 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) + + 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 diff --git a/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs b/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs index 919fe7a6744..9c7ded3ae5a 100644 --- a/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs +++ b/vsintegration/src/FSharp.Editor/Common/CommonHelpers.fs @@ -319,7 +319,7 @@ module internal CommonHelpers = try let textLine = sourceText.Lines.GetLineFromPosition(position) let textLinePos = sourceText.Lines.GetLinePosition(position) - let lineNumber = textLinePos.Line + 1 // FCS line number + let lineNumber = textLinePos.Line let sourceTokenizer = FSharpSourceTokenizer(defines, Some fileName) let lines = sourceText.Lines // We keep incremental data per-document. When text changes we correlate text line-by-line (by hash codes of lines) @@ -328,11 +328,11 @@ module internal CommonHelpers = // Go backwards to find the last cached scanned line that is valid let scanStartLine = let mutable i = lineNumber - while i > 0 && (match sourceTextData.[i-1] with Some data -> not (data.IsValid(lines.[i])) | None -> true) do + while i > 0 && (match sourceTextData.[i] with Some data -> not (data.IsValid(lines.[i])) | None -> true) do i <- i - 1 i - let lexState = if scanStartLine = 0 then 0L else sourceTextData.[scanStartLine - 1].Value.LexStateAtEndOfLine + let lexState = if scanStartLine = 0 then 0L else sourceTextData.[scanStartLine].Value.LexStateAtEndOfLine let lineContents = textLine.Text.ToString(textLine.Span) let lineData = @@ -477,30 +477,6 @@ module internal Extensions = isPrivate && declaredInTheFile - let glyphMajorToRoslynGlyph = function - | GlyphMajor.Class -> Glyph.ClassPublic - | GlyphMajor.Constant -> Glyph.ConstantPublic - | GlyphMajor.Delegate -> Glyph.DelegatePublic - | GlyphMajor.Enum -> Glyph.EnumPublic - | GlyphMajor.EnumMember -> Glyph.FieldPublic - | GlyphMajor.Event -> Glyph.EventPublic - | GlyphMajor.Exception -> Glyph.ClassPublic - | GlyphMajor.FieldBlue -> Glyph.FieldPublic - | GlyphMajor.Interface -> Glyph.InterfacePublic - | GlyphMajor.Method -> Glyph.MethodPublic - | GlyphMajor.Method2 -> Glyph.MethodPublic - | GlyphMajor.Module -> Glyph.ModulePublic - | GlyphMajor.NameSpace -> Glyph.Namespace - | GlyphMajor.Property -> Glyph.PropertyPublic - | GlyphMajor.Struct -> Glyph.StructurePublic - | GlyphMajor.Typedef -> Glyph.ClassPublic - | GlyphMajor.Type -> Glyph.ClassPublic - | GlyphMajor.Union -> Glyph.EnumPublic - | GlyphMajor.Variable -> Glyph.FieldPublic - | GlyphMajor.ValueType -> Glyph.StructurePublic - | 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. diff --git a/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs b/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs new file mode 100644 index 00000000000..665ed8749ac --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Common/SymbolHelpers.fs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Collections.Generic +open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks +open System.Runtime.CompilerServices + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Classification +open Microsoft.CodeAnalysis.Text + +open Microsoft.VisualStudio.FSharp.LanguageService +open Microsoft.FSharp.Compiler +open Microsoft.FSharp.Compiler.SourceCodeServices +open Microsoft.FSharp.Compiler.SourceCodeServices.ItemDescriptionIcons + + +module internal SymbolHelpers = + let getSymbolUsesInSolution (symbol: FSharpSymbol, declLoc: SymbolDeclarationLocation, checkFileResults: FSharpCheckFileResults, + projectInfoManager: ProjectInfoManager, checker: FSharpChecker, solution: Solution) = + async { + let! symbolUses = + match declLoc with + | SymbolDeclarationLocation.CurrentDocument -> + checkFileResults.GetUsesOfSymbolInFile(symbol) + | SymbolDeclarationLocation.Projects (projects, isInternalToProject) -> + let projects = + if isInternalToProject then projects + else + [ for project in projects do + yield project + yield! project.GetDependentProjects() ] + |> List.distinctBy (fun x -> x.Id) + + projects + |> Seq.map (fun project -> + async { + match projectInfoManager.TryGetOptionsForProject(project.Id) with + | Some options -> + let! projectCheckResults = checker.ParseAndCheckProject(options) + return! projectCheckResults.GetUsesOfSymbol(symbol) + | None -> return [||] + }) + |> Async.Parallel + |> Async.Map Array.concat + + return + (symbolUses + |> Seq.collect (fun symbolUse -> + solution.GetDocumentIdsWithFilePath(symbolUse.FileName) |> Seq.map (fun id -> id, symbolUse)) + |> Seq.groupBy fst + ).ToImmutableDictionary( + (fun (id, _) -> id), + fun (_, xs) -> xs |> Seq.map snd |> Seq.toArray) + } + + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 98aba4be12a..811cdad75fc 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -1,4 +1,4 @@ - + @@ -38,6 +38,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs index f07d57211c8..272cbd0ee3f 100644 --- a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs +++ b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs @@ -91,42 +91,9 @@ type internal InlineRenameInfo let span = CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate) CommonHelpers.fixupSpan(sourceText, span) - let symbolUses = - async { - let! symbolUses = - match declLoc with - | SymbolDeclarationLocation.CurrentDocument -> - checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol) - | SymbolDeclarationLocation.Projects (projects, isInternalToProject) -> - let projects = - if isInternalToProject then projects - else - [ for project in projects do - yield project - yield! project.GetDependentProjects() ] - |> List.distinctBy (fun x -> x.Id) - - projects - |> Seq.map (fun project -> - async { - match projectInfoManager.TryGetOptionsForProject(project.Id) with - | Some options -> - let! projectCheckResults = checker.ParseAndCheckProject(options) - return! projectCheckResults.GetUsesOfSymbol(symbolUse.Symbol) - | None -> return [||] - }) - |> Async.Parallel - |> Async.Map Array.concat - - return - (symbolUses - |> Seq.collect (fun symbolUse -> - document.Project.Solution.GetDocumentIdsWithFilePath(symbolUse.FileName) |> Seq.map (fun id -> id, symbolUse)) - |> Seq.groupBy fst - ).ToImmutableDictionary( - (fun (id, _) -> id), - fun (_, xs) -> xs |> Seq.map snd |> Seq.toArray) - } |> Async.Cache + let symbolUses = + SymbolHelpers.getSymbolUsesInSolution(symbolUse.Symbol, declLoc, checkFileResults, projectInfoManager, checker, document.Project.Solution) + |> Async.Cache interface IInlineRenameInfo with member __.CanRename = true @@ -191,15 +158,14 @@ type internal InlineRenameService match checkFileAnswer with | FSharpCheckFileAnswer.Aborted -> return FailureInlineRenameInfo.Instance | FSharpCheckFileAnswer.Succeeded(checkFileResults) -> - - let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text]) - - match symbolUse with - | Some symbolUse -> - match symbolUse.GetDeclarationLocation(document) with - | Some declLoc -> return InlineRenameInfo(checker, projectInfoManager, document, sourceText, symbolUse, declLoc, checkFileResults) :> IInlineRenameInfo + let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, symbol.RightColumn, textLine.Text.ToString(), [symbol.Text]) + + match symbolUse with + | Some symbolUse -> + match symbolUse.GetDeclarationLocation(document) with + | Some declLoc -> return InlineRenameInfo(checker, projectInfoManager, document, sourceText, symbolUse, declLoc, checkFileResults) :> IInlineRenameInfo + | _ -> return FailureInlineRenameInfo.Instance | _ -> return FailureInlineRenameInfo.Instance - | _ -> return FailureInlineRenameInfo.Instance | None -> return FailureInlineRenameInfo.Instance }