Skip to content

Commit

Permalink
Implement inline rename (#1932)
Browse files Browse the repository at this point in the history
* 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 af81683.

* 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
  • Loading branch information
vasily-kirichenko authored and KevinRansom committed Dec 16, 2016
1 parent 6dd77c7 commit 1d65544
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 32 deletions.
21 changes: 21 additions & 0 deletions vsintegration/src/FSharp.Editor/CommonHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Collections.Generic
open System.Threading
open System.Threading.Tasks
Expand Down Expand Up @@ -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<AsyncReplyChannel<_>>.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
}
29 changes: 28 additions & 1 deletion vsintegration/src/FSharp.Editor/CommonRoslynHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -60,11 +61,37 @@ 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

[<AutoOpen>]
module internal RoslynExtensions =
type Project with
/// The list of all other projects within the same solution that reference this project.
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 ]
29 changes: 1 addition & 28 deletions vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs
Original file line number Diff line number Diff line change
Expand Up @@ -108,33 +108,7 @@ type internal FSharpCompletionProvider
let results = List<CompletionItem>()

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)
Expand All @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,14 @@
<Compile Include="Diagnostics\ProjectDiagnosticAnalyzer.fs"/>
<Compile Include="Completion\CompletionProvider.fs"/>
<Compile Include="Completion\SignatureHelp.fs"/>
<Compile Include="BlockComment\CommentUncommentService.fs"/>
<Compile Include="QuickInfo\QuickInfoProvider.fs"/>
<Compile Include="InlineRename\InlineRenameService.fs" />
<Compile Include="DocumentHighlights\DocumentHighlightsService.fs" />
<Compile Include="HelpContextService.fs"/>
<Compile Include="Navigation\GoToDefinitionService.fs" />
<Compile Include="Navigation\NavigationBarItemService.fs" />
<Compile Include="Navigation\NavigateToSearchService.fs" />
<Compile Include="Navigation\FindReferencesService.fs" />
<Compile Include="BlockComment\CommentUncommentService.fs" />
<Compile Include="QuickInfo\QuickInfoProvider.fs" />
<Compile Include="Structure\BlockStructureService.fs" />
<Compile Include="HelpContextService.fs" />
<Compile Include="ContentType.fs" />
Expand Down
217 changes: 217 additions & 0 deletions vsintegration/src/FSharp.Editor/InlineRename/InlineRenameService.fs
Original file line number Diff line number Diff line change
@@ -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<IInlineRenameLocationSet>.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<InlineRenameLocation> =
[| for doc in locationsByDocument do yield! doc.Locations |] :> _

member this.GetReplacementsAsync(replacementText, _optionSet, cancellationToken) : Task<IInlineRenameReplacementInfo> =
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

[<ExportLanguageService(typeof<IEditorInlineRenameService>, FSharpCommonConstants.FSharpLanguageName); Shared>]
type internal InlineRenameService
[<ImportingConstructor>]
(
projectInfoManager: ProjectInfoManager,
checkerProvider: FSharpCheckerProvider,
[<ImportMany>] _refactorNotifyServices: seq<IRefactorNotifyService>
) =

static member GetInlineRenameInfo(checker: FSharpChecker, projectInfoManager: ProjectInfoManager, document: Document, sourceText: SourceText, position: int,
defines: string list, options: FSharpProjectOptions, textVersionHash: int, cancellationToken: CancellationToken) : Async<IInlineRenameInfo> =
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<IInlineRenameInfo> =
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)

0 comments on commit 1d65544

Please sign in to comment.