Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement inline rename #1932

Merged
merged 28 commits into from
Dec 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e1c00eb
add InlineRenameService skeleton
vasily-kirichenko Dec 3, 2016
e40465f
implement IEditorInlineRenameService instead of inheriting AbstractEd…
vasily-kirichenko Dec 3, 2016
45c9836
add some real logic into InlineRenameService
vasily-kirichenko Dec 3, 2016
4a6264f
everything in place, but does not work yet
vasily-kirichenko Dec 3, 2016
cc7d940
TryOnBefore/AfterGlobalSymbolRename should return `true`
vasily-kirichenko Dec 4, 2016
36c2222
some progress
vasily-kirichenko Dec 4, 2016
a2c4476
port comments
vasily-kirichenko Dec 4, 2016
5346a1a
add RenamedSpansTracker
vasily-kirichenko Dec 4, 2016
4a6b6bf
trying to adjust span positions
vasily-kirichenko Dec 4, 2016
ea94cc5
fixes
vasily-kirichenko Dec 4, 2016
5d94876
do not check whole project in renamed symbol is local for file
vasily-kirichenko Dec 4, 2016
c4a2058
update inline rename implementation
vladima Dec 6, 2016
06c310e
remove RenamedSpansTracker
vasily-kirichenko Dec 7, 2016
85393ba
fixed: CommonHelpers.tryClassifyAtPosition return wrong result at the…
vasily-kirichenko Dec 7, 2016
0b83082
do not fix spans for now
vasily-kirichenko Dec 7, 2016
428ce7c
Revert "fixed: CommonHelpers.tryClassifyAtPosition return wrong resul…
vasily-kirichenko Dec 8, 2016
fe67eb9
refactoring
vasily-kirichenko Dec 8, 2016
ba61bcb
Merge remote-tracking branch 'origin/master' into inline-rename
vasily-kirichenko Dec 8, 2016
3cae0d7
refactoring
vasily-kirichenko Dec 8, 2016
9c2f1a7
use Async.Cache instead of mutable Task reference to emulate a non au…
vasily-kirichenko Dec 9, 2016
de18919
solution wide Inline Rename (wip)
vasily-kirichenko Dec 9, 2016
38513b6
Merge branch 'master' into inline-rename
Dec 14, 2016
5225264
fix after merge
Dec 14, 2016
3efeaab
update Microsoft.CodeAnalysis.xxx packages to 2.0.0-rc
Dec 14, 2016
ac91958
Merge remote-tracking branch 'origin/master' into inline-rename
Dec 15, 2016
c2a4f55
fix after merge
Dec 15, 2016
bbc6238
Merge branch 'master' into inline-rename
Dec 16, 2016
337bb65
Merge branch 'master' into inline-rename
KevinRansom Dec 16, 2016
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
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)