From 1d655445fc7ce73edaa426b72b0afab3adb8724b Mon Sep 17 00:00:00 2001 From: Vasily Kirichenko Date: Fri, 16 Dec 2016 21:13:20 +0300 Subject: [PATCH] Implement inline rename (#1932) * add InlineRenameService skeleton * implement IEditorInlineRenameService instead of inheriting AbstractEditorInlineRenameService, which is roslyn-AST bound * add some real logic into InlineRenameService * everything in place, but does not work yet * TryOnBefore/AfterGlobalSymbolRename should return `true` * some progress * port comments * add RenamedSpansTracker * trying to adjust span positions * fixes * do not check whole project in renamed symbol is local for file * update inline rename implementation * remove RenamedSpansTracker * fixed: CommonHelpers.tryClassifyAtPosition return wrong result at the end position * do not fix spans for now * Revert "fixed: CommonHelpers.tryClassifyAtPosition return wrong result at the end position" This reverts commit af816832511f82458447505806a5aae44726664b. * refactoring * refactoring * use Async.Cache instead of mutable Task reference to emulate a non auto-starting promise * solution wide Inline Rename (wip) * fix after merge * update Microsoft.CodeAnalysis.xxx packages to 2.0.0-rc * fix after merge --- .../src/FSharp.Editor/CommonHelpers.fs | 21 ++ .../src/FSharp.Editor/CommonRoslynHelpers.fs | 29 ++- .../Completion/CompletionProvider.fs | 29 +-- .../src/FSharp.Editor/FSharp.Editor.fsproj | 5 +- .../InlineRename/InlineRenameService.fs | 217 ++++++++++++++++++ 5 files changed, 269 insertions(+), 32 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs diff --git a/vsintegration/src/FSharp.Editor/CommonHelpers.fs b/vsintegration/src/FSharp.Editor/CommonHelpers.fs index 65978303188..914c30f1f4b 100644 --- a/vsintegration/src/FSharp.Editor/CommonHelpers.fs +++ b/vsintegration/src/FSharp.Editor/CommonHelpers.fs @@ -2,6 +2,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor +open System open System.Collections.Generic open System.Threading open System.Threading.Tasks @@ -341,3 +342,23 @@ 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 + } diff --git a/vsintegration/src/FSharp.Editor/CommonRoslynHelpers.fs b/vsintegration/src/FSharp.Editor/CommonRoslynHelpers.fs index b7eeb6b1c34..48c243d3ccf 100644 --- a/vsintegration/src/FSharp.Editor/CommonRoslynHelpers.fs +++ b/vsintegration/src/FSharp.Editor/CommonRoslynHelpers.fs @@ -9,6 +9,7 @@ open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.FSharp.Compiler open Microsoft.FSharp.Compiler.SourceCodeServices +open Microsoft.FSharp.Compiler.SourceCodeServices.ItemDescriptionIcons open Microsoft.FSharp.Compiler.Range open Microsoft.VisualStudio.FSharp.LanguageService @@ -60,6 +61,32 @@ module internal CommonRoslynHelpers = let descriptor = new DiagnosticDescriptor(id, emptyString, description, error.Subcategory, severity, true, emptyString, String.Empty, null) Diagnostic.Create(descriptor, location) + let FSharpGlyphToRoslynGlyph = function + // FSROSLYNTODO: This doesn't yet reflect pulbic/private/internal into the glyph + // FSROSLYNTODO: We should really use FSharpSymbol information here. But GetDeclarationListInfo doesn't provide it, and switch to GetDeclarationListSymbols is a bit large at the moment + | GlyphMajor.Class -> Glyph.ClassPublic + | GlyphMajor.Constant -> Glyph.ConstantPublic + | GlyphMajor.Delegate -> Glyph.DelegatePublic + | GlyphMajor.Enum -> Glyph.EnumPublic + | GlyphMajor.EnumMember -> Glyph.EnumMember + | GlyphMajor.Event -> Glyph.EventPublic + | GlyphMajor.Exception -> Glyph.ClassPublic + | GlyphMajor.FieldBlue -> Glyph.FieldPublic + | GlyphMajor.Interface -> Glyph.InterfacePublic + | GlyphMajor.Method -> Glyph.MethodPublic + | GlyphMajor.Method2 -> Glyph.ExtensionMethodPublic + | 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.Local + | GlyphMajor.ValueType -> Glyph.StructurePublic + | GlyphMajor.Error -> Glyph.Error + | _ -> Glyph.ClassPublic + [] module internal RoslynExtensions = type Project with @@ -67,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 ] \ No newline at end of file + yield project ] diff --git a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs index 0372bbf707f..052321b4ae6 100644 --- a/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs +++ b/vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs @@ -108,33 +108,7 @@ type internal FSharpCompletionProvider let results = List() for declarationItem in declarations.Items do - // FSROSLYNTODO: This doesn't yet reflect pulbic/private/internal into the glyph - // FSROSLYNTODO: We should really use FSharpSymbol information here. But GetDeclarationListInfo doesn't provide it, and switch to GetDeclarationListSymbols is a bit large at the moment - let glyph = - match declarationItem.GlyphMajor with - | GlyphMajor.Class -> Glyph.ClassPublic - | GlyphMajor.Constant -> Glyph.ConstantPublic - | GlyphMajor.Delegate -> Glyph.DelegatePublic - | GlyphMajor.Enum -> Glyph.EnumPublic - | GlyphMajor.EnumMember -> Glyph.EnumMember - | GlyphMajor.Event -> Glyph.EventPublic - | GlyphMajor.Exception -> Glyph.ClassPublic - | GlyphMajor.FieldBlue -> Glyph.FieldPublic - | GlyphMajor.Interface -> Glyph.InterfacePublic - | GlyphMajor.Method -> Glyph.MethodPublic - | GlyphMajor.Method2 -> Glyph.ExtensionMethodPublic - | 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.Local - | GlyphMajor.ValueType -> Glyph.StructurePublic - | GlyphMajor.Error -> Glyph.Error - | _ -> Glyph.ClassPublic - + let glyph = CommonRoslynHelpers.FSharpGlyphToRoslynGlyph declarationItem.GlyphMajor let completionItem = CommonCompletionItem.Create(declarationItem.Name, glyph=Nullable(glyph)) declarationItemsCache.Remove(completionItem.DisplayText) |> ignore // clear out stale entries if they exist declarationItemsCache.Add(completionItem.DisplayText, declarationItem) @@ -143,7 +117,6 @@ type internal FSharpCompletionProvider return results } - override this.ShouldTriggerCompletion(sourceText: SourceText, caretPosition: int, trigger: CompletionTrigger, _: OptionSet) = let getInfo() = let documentId = workspace.GetDocumentIdInCurrentContext(sourceText.Container) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index c7c58c6db64..cb38bbc2ea6 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -41,15 +41,14 @@ - + - + - diff --git a/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs new file mode 100644 index 00000000000..68907c5bdfe --- /dev/null +++ b/vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs @@ -0,0 +1,217 @@ +// 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.Composition +open System.Collections.Generic +open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Editor +open Microsoft.CodeAnalysis.Host.Mef +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.Editor.Implementation.InlineRename + +open Microsoft.FSharp.Compiler +open Microsoft.FSharp.Compiler.Parser +open Microsoft.FSharp.Compiler.Range +open Microsoft.FSharp.Compiler.SourceCodeServices + +type internal FailureInlineRenameInfo private () = + interface IInlineRenameInfo with + member __.CanRename = false + member __.LocalizedErrorMessage = EditorFeaturesResources.You_cannot_rename_this_element + member __.TriggerSpan = Unchecked.defaultof<_> + member __.HasOverloads = false + member __.ForceRenameOverloads = true + member __.DisplayName = "" + member __.FullDisplayName = "" + member __.Glyph = Glyph.MethodPublic + member __.GetFinalSymbolName _replacementText = "" + member __.GetReferenceEditSpan(_location, _cancellationToken) = Unchecked.defaultof<_> + member __.GetConflictEditSpan(_location, _replacementText, _cancellationToken) = Nullable() + member __.FindRenameLocationsAsync(_optionSet, _cancellationToken) = Task.FromResult null + member __.TryOnBeforeGlobalSymbolRenamed(_workspace, _changedDocumentIDs, _replacementText) = false + member __.TryOnAfterGlobalSymbolRenamed(_workspace, _changedDocumentIDs, _replacementText) = false + static member Instance = FailureInlineRenameInfo() + +type internal DocumentLocations = + { Document: Document + Locations: InlineRenameLocation [] } + +type internal InlineRenameLocationSet(locationsByDocument: DocumentLocations [], originalSolution: Solution) = + interface IInlineRenameLocationSet with + member __.Locations : IList = + [| for doc in locationsByDocument do yield! doc.Locations |] :> _ + + member this.GetReplacementsAsync(replacementText, _optionSet, cancellationToken) : Task = + let rec applyChanges i (solution: Solution) = + async { + if i = locationsByDocument.Length then + return solution + else + let doc = locationsByDocument.[i] + let! oldSourceText = doc.Document.GetTextAsync(cancellationToken) |> Async.AwaitTask + let changes = doc.Locations |> Seq.map (fun loc -> TextChange(loc.TextSpan, replacementText)) + let newSource = oldSourceText.WithChanges(changes) + return! applyChanges (i + 1) (solution.WithDocumentText(doc.Document.Id, newSource)) + } + + async { + let! newSolution = applyChanges 0 originalSolution + return + { new IInlineRenameReplacementInfo with + member __.NewSolution = newSolution + member __.ReplacementTextValid = true + member __.DocumentIds = locationsByDocument |> Seq.map (fun doc -> doc.Document.Id) + member __.GetReplacements(documentId) = Seq.empty } + } + |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken) + +type internal InlineRenameInfo + ( + checker: FSharpChecker, + projectInfoManager: ProjectInfoManager, + document: Document, + sourceText: SourceText, + symbolUse: FSharpSymbolUse, + declLoc: SymbolDeclarationLocation, + checkFileResults: FSharpCheckFileResults + ) = + + let getDocumentText (document: Document) cancellationToken = + match document.TryGetText() with + | true, text -> text + | _ -> document.GetTextAsync(cancellationToken).Result + + let triggerSpan = + 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 + + interface IInlineRenameInfo with + member __.CanRename = true + member __.LocalizedErrorMessage = null + member __.TriggerSpan = triggerSpan + member __.HasOverloads = false + member __.ForceRenameOverloads = true + member __.DisplayName = symbolUse.Symbol.DisplayName + member __.FullDisplayName = try symbolUse.Symbol.FullName with _ -> symbolUse.Symbol.DisplayName + member __.Glyph = Glyph.MethodPublic + member __.GetFinalSymbolName replacementText = replacementText + + member __.GetReferenceEditSpan(location, cancellationToken) = + let text = getDocumentText location.Document cancellationToken + CommonHelpers.fixupSpan(text, location.TextSpan) + + member __.GetConflictEditSpan(location, _replacementText, _cancellationToken) = Nullable(location.TextSpan) + + member __.FindRenameLocationsAsync(_optionSet, cancellationToken) = + async { + let! symbolUsesByDocumentId = symbolUses + let! locationsByDocument = + symbolUsesByDocumentId + |> Seq.map (fun (KeyValue(documentId, symbolUses)) -> + async { + let document = document.Project.Solution.GetDocument(documentId) + let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask + let locations = + symbolUses + |> Array.map (fun symbolUse -> + let textSpan = CommonHelpers.fixupSpan(sourceText, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate)) + InlineRenameLocation(document, textSpan)) + return { Document = document; Locations = locations } + }) + |> Async.Parallel + return InlineRenameLocationSet(locationsByDocument, document.Project.Solution) :> IInlineRenameLocationSet + } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken) + + member __.TryOnBeforeGlobalSymbolRenamed(_workspace, _changedDocumentIDs, _replacementText) = true + member __.TryOnAfterGlobalSymbolRenamed(_workspace, _changedDocumentIDs, _replacementText) = true + +[, FSharpCommonConstants.FSharpLanguageName); Shared>] +type internal InlineRenameService + [] + ( + projectInfoManager: ProjectInfoManager, + checkerProvider: FSharpCheckerProvider, + [] _refactorNotifyServices: seq + ) = + + static member GetInlineRenameInfo(checker: FSharpChecker, projectInfoManager: ProjectInfoManager, document: Document, sourceText: SourceText, position: int, + defines: string list, options: FSharpProjectOptions, textVersionHash: int, cancellationToken: CancellationToken) : Async = + async { + let textLine = sourceText.Lines.GetLineFromPosition(position) + let textLinePos = sourceText.Lines.GetLinePosition(position) + let fcsTextLineNumber = textLinePos.Line + 1 // Roslyn line numbers are zero-based, FSharp.Compiler.Service line numbers are 1-based + + match CommonHelpers.tryClassifyAtPosition(document.Id, sourceText, document.FilePath, defines, position, cancellationToken) with + | Some (islandColumn, qualifiers, _) -> + let! _parseResults, checkFileAnswer = checker.ParseAndCheckFileInProject(document.FilePath, textVersionHash, sourceText.ToString(), options) + + match checkFileAnswer with + | FSharpCheckFileAnswer.Aborted -> return FailureInlineRenameInfo.Instance :> _ + | FSharpCheckFileAnswer.Succeeded(checkFileResults) -> + + let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, islandColumn, textLine.Text.ToString(), qualifiers) + + match symbolUse with + | Some symbolUse -> + match symbolUse.GetDeclarationLocation(document) with + | Some declLoc -> return InlineRenameInfo(checker, projectInfoManager, document, sourceText, symbolUse, declLoc, checkFileResults) :> _ + | _ -> return FailureInlineRenameInfo.Instance :> _ + | _ -> return FailureInlineRenameInfo.Instance :> _ + | None -> return FailureInlineRenameInfo.Instance :> _ + } + + interface IEditorInlineRenameService with + member __.GetRenameInfoAsync(document: Document, position: int, cancellationToken: CancellationToken) : Task = + async { + match projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) with + | Some options -> + let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask + let! textVersion = document.GetTextVersionAsync(cancellationToken) |> Async.AwaitTask + let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList) + return! InlineRenameService.GetInlineRenameInfo(checkerProvider.Checker, projectInfoManager, document, sourceText, position, defines, options, textVersion.GetHashCode(), cancellationToken) + | None -> return FailureInlineRenameInfo.Instance :> _ + } + |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken) \ No newline at end of file