Skip to content

Commit 1d65544

Browse files
vasily-kirichenkoKevinRansom
authored andcommitted
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 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
1 parent 6dd77c7 commit 1d65544

File tree

5 files changed

+269
-32
lines changed

5 files changed

+269
-32
lines changed

vsintegration/src/FSharp.Editor/CommonHelpers.fs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Microsoft.VisualStudio.FSharp.Editor
44

5+
open System
56
open System.Collections.Generic
67
open System.Threading
78
open System.Threading.Tasks
@@ -341,3 +342,23 @@ module internal Extensions =
341342
| GlyphMajor.Error -> Glyph.Error
342343
| _ -> Glyph.None
343344

345+
type Async<'a> with
346+
/// Creates an asynchronous workflow that runs the asynchronous workflow given as an argument at most once.
347+
/// When the returned workflow is started for the second time, it reuses the result of the previous execution.
348+
static member Cache (input : Async<'T>) =
349+
let agent = MailboxProcessor<AsyncReplyChannel<_>>.Start <| fun agent ->
350+
async {
351+
let! replyCh = agent.Receive ()
352+
let! res = input
353+
replyCh.Reply res
354+
while true do
355+
let! replyCh = agent.Receive ()
356+
replyCh.Reply res
357+
}
358+
async { return! agent.PostAndAsyncReply id }
359+
360+
static member inline Map (f: 'a -> 'b) (input: Async<'a>) : Async<'b> =
361+
async {
362+
let! result = input
363+
return f result
364+
}

vsintegration/src/FSharp.Editor/CommonRoslynHelpers.fs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ open Microsoft.CodeAnalysis
99
open Microsoft.CodeAnalysis.Text
1010
open Microsoft.FSharp.Compiler
1111
open Microsoft.FSharp.Compiler.SourceCodeServices
12+
open Microsoft.FSharp.Compiler.SourceCodeServices.ItemDescriptionIcons
1213
open Microsoft.FSharp.Compiler.Range
1314
open Microsoft.VisualStudio.FSharp.LanguageService
1415

@@ -60,11 +61,37 @@ module internal CommonRoslynHelpers =
6061
let descriptor = new DiagnosticDescriptor(id, emptyString, description, error.Subcategory, severity, true, emptyString, String.Empty, null)
6162
Diagnostic.Create(descriptor, location)
6263

64+
let FSharpGlyphToRoslynGlyph = function
65+
// FSROSLYNTODO: This doesn't yet reflect pulbic/private/internal into the glyph
66+
// 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
67+
| GlyphMajor.Class -> Glyph.ClassPublic
68+
| GlyphMajor.Constant -> Glyph.ConstantPublic
69+
| GlyphMajor.Delegate -> Glyph.DelegatePublic
70+
| GlyphMajor.Enum -> Glyph.EnumPublic
71+
| GlyphMajor.EnumMember -> Glyph.EnumMember
72+
| GlyphMajor.Event -> Glyph.EventPublic
73+
| GlyphMajor.Exception -> Glyph.ClassPublic
74+
| GlyphMajor.FieldBlue -> Glyph.FieldPublic
75+
| GlyphMajor.Interface -> Glyph.InterfacePublic
76+
| GlyphMajor.Method -> Glyph.MethodPublic
77+
| GlyphMajor.Method2 -> Glyph.ExtensionMethodPublic
78+
| GlyphMajor.Module -> Glyph.ModulePublic
79+
| GlyphMajor.NameSpace -> Glyph.Namespace
80+
| GlyphMajor.Property -> Glyph.PropertyPublic
81+
| GlyphMajor.Struct -> Glyph.StructurePublic
82+
| GlyphMajor.Typedef -> Glyph.ClassPublic
83+
| GlyphMajor.Type -> Glyph.ClassPublic
84+
| GlyphMajor.Union -> Glyph.EnumPublic
85+
| GlyphMajor.Variable -> Glyph.Local
86+
| GlyphMajor.ValueType -> Glyph.StructurePublic
87+
| GlyphMajor.Error -> Glyph.Error
88+
| _ -> Glyph.ClassPublic
89+
6390
[<AutoOpen>]
6491
module internal RoslynExtensions =
6592
type Project with
6693
/// The list of all other projects within the same solution that reference this project.
6794
member this.GetDependentProjects() =
6895
[ for project in this.Solution.Projects do
6996
if project.ProjectReferences |> Seq.exists (fun ref -> ref.ProjectId = this.Id) then
70-
yield project ]
97+
yield project ]

vsintegration/src/FSharp.Editor/Completion/CompletionProvider.fs

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -108,33 +108,7 @@ type internal FSharpCompletionProvider
108108
let results = List<CompletionItem>()
109109

110110
for declarationItem in declarations.Items do
111-
// FSROSLYNTODO: This doesn't yet reflect pulbic/private/internal into the glyph
112-
// 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
113-
let glyph =
114-
match declarationItem.GlyphMajor with
115-
| GlyphMajor.Class -> Glyph.ClassPublic
116-
| GlyphMajor.Constant -> Glyph.ConstantPublic
117-
| GlyphMajor.Delegate -> Glyph.DelegatePublic
118-
| GlyphMajor.Enum -> Glyph.EnumPublic
119-
| GlyphMajor.EnumMember -> Glyph.EnumMember
120-
| GlyphMajor.Event -> Glyph.EventPublic
121-
| GlyphMajor.Exception -> Glyph.ClassPublic
122-
| GlyphMajor.FieldBlue -> Glyph.FieldPublic
123-
| GlyphMajor.Interface -> Glyph.InterfacePublic
124-
| GlyphMajor.Method -> Glyph.MethodPublic
125-
| GlyphMajor.Method2 -> Glyph.ExtensionMethodPublic
126-
| GlyphMajor.Module -> Glyph.ModulePublic
127-
| GlyphMajor.NameSpace -> Glyph.Namespace
128-
| GlyphMajor.Property -> Glyph.PropertyPublic
129-
| GlyphMajor.Struct -> Glyph.StructurePublic
130-
| GlyphMajor.Typedef -> Glyph.ClassPublic
131-
| GlyphMajor.Type -> Glyph.ClassPublic
132-
| GlyphMajor.Union -> Glyph.EnumPublic
133-
| GlyphMajor.Variable -> Glyph.Local
134-
| GlyphMajor.ValueType -> Glyph.StructurePublic
135-
| GlyphMajor.Error -> Glyph.Error
136-
| _ -> Glyph.ClassPublic
137-
111+
let glyph = CommonRoslynHelpers.FSharpGlyphToRoslynGlyph declarationItem.GlyphMajor
138112
let completionItem = CommonCompletionItem.Create(declarationItem.Name, glyph=Nullable(glyph))
139113
declarationItemsCache.Remove(completionItem.DisplayText) |> ignore // clear out stale entries if they exist
140114
declarationItemsCache.Add(completionItem.DisplayText, declarationItem)
@@ -143,7 +117,6 @@ type internal FSharpCompletionProvider
143117
return results
144118
}
145119

146-
147120
override this.ShouldTriggerCompletion(sourceText: SourceText, caretPosition: int, trigger: CompletionTrigger, _: OptionSet) =
148121
let getInfo() =
149122
let documentId = workspace.GetDocumentIdInCurrentContext(sourceText.Container)

vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,14 @@
4141
<Compile Include="Diagnostics\ProjectDiagnosticAnalyzer.fs"/>
4242
<Compile Include="Completion\CompletionProvider.fs"/>
4343
<Compile Include="Completion\SignatureHelp.fs"/>
44-
<Compile Include="BlockComment\CommentUncommentService.fs"/>
4544
<Compile Include="QuickInfo\QuickInfoProvider.fs"/>
45+
<Compile Include="InlineRename\InlineRenameService.fs" />
4646
<Compile Include="DocumentHighlights\DocumentHighlightsService.fs" />
47-
<Compile Include="HelpContextService.fs"/>
4847
<Compile Include="Navigation\GoToDefinitionService.fs" />
4948
<Compile Include="Navigation\NavigationBarItemService.fs" />
5049
<Compile Include="Navigation\NavigateToSearchService.fs" />
50+
<Compile Include="Navigation\FindReferencesService.fs" />
5151
<Compile Include="BlockComment\CommentUncommentService.fs" />
52-
<Compile Include="QuickInfo\QuickInfoProvider.fs" />
5352
<Compile Include="Structure\BlockStructureService.fs" />
5453
<Compile Include="HelpContextService.fs" />
5554
<Compile Include="ContentType.fs" />
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// 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.
2+
3+
namespace Microsoft.VisualStudio.FSharp.Editor
4+
5+
open System
6+
open System.Composition
7+
open System.Collections.Generic
8+
open System.Collections.Immutable
9+
open System.Threading
10+
open System.Threading.Tasks
11+
12+
open Microsoft.CodeAnalysis
13+
open Microsoft.CodeAnalysis.Editor
14+
open Microsoft.CodeAnalysis.Host.Mef
15+
open Microsoft.CodeAnalysis.Text
16+
open Microsoft.CodeAnalysis.Editor.Implementation.InlineRename
17+
18+
open Microsoft.FSharp.Compiler
19+
open Microsoft.FSharp.Compiler.Parser
20+
open Microsoft.FSharp.Compiler.Range
21+
open Microsoft.FSharp.Compiler.SourceCodeServices
22+
23+
type internal FailureInlineRenameInfo private () =
24+
interface IInlineRenameInfo with
25+
member __.CanRename = false
26+
member __.LocalizedErrorMessage = EditorFeaturesResources.You_cannot_rename_this_element
27+
member __.TriggerSpan = Unchecked.defaultof<_>
28+
member __.HasOverloads = false
29+
member __.ForceRenameOverloads = true
30+
member __.DisplayName = ""
31+
member __.FullDisplayName = ""
32+
member __.Glyph = Glyph.MethodPublic
33+
member __.GetFinalSymbolName _replacementText = ""
34+
member __.GetReferenceEditSpan(_location, _cancellationToken) = Unchecked.defaultof<_>
35+
member __.GetConflictEditSpan(_location, _replacementText, _cancellationToken) = Nullable()
36+
member __.FindRenameLocationsAsync(_optionSet, _cancellationToken) = Task<IInlineRenameLocationSet>.FromResult null
37+
member __.TryOnBeforeGlobalSymbolRenamed(_workspace, _changedDocumentIDs, _replacementText) = false
38+
member __.TryOnAfterGlobalSymbolRenamed(_workspace, _changedDocumentIDs, _replacementText) = false
39+
static member Instance = FailureInlineRenameInfo()
40+
41+
type internal DocumentLocations =
42+
{ Document: Document
43+
Locations: InlineRenameLocation [] }
44+
45+
type internal InlineRenameLocationSet(locationsByDocument: DocumentLocations [], originalSolution: Solution) =
46+
interface IInlineRenameLocationSet with
47+
member __.Locations : IList<InlineRenameLocation> =
48+
[| for doc in locationsByDocument do yield! doc.Locations |] :> _
49+
50+
member this.GetReplacementsAsync(replacementText, _optionSet, cancellationToken) : Task<IInlineRenameReplacementInfo> =
51+
let rec applyChanges i (solution: Solution) =
52+
async {
53+
if i = locationsByDocument.Length then
54+
return solution
55+
else
56+
let doc = locationsByDocument.[i]
57+
let! oldSourceText = doc.Document.GetTextAsync(cancellationToken) |> Async.AwaitTask
58+
let changes = doc.Locations |> Seq.map (fun loc -> TextChange(loc.TextSpan, replacementText))
59+
let newSource = oldSourceText.WithChanges(changes)
60+
return! applyChanges (i + 1) (solution.WithDocumentText(doc.Document.Id, newSource))
61+
}
62+
63+
async {
64+
let! newSolution = applyChanges 0 originalSolution
65+
return
66+
{ new IInlineRenameReplacementInfo with
67+
member __.NewSolution = newSolution
68+
member __.ReplacementTextValid = true
69+
member __.DocumentIds = locationsByDocument |> Seq.map (fun doc -> doc.Document.Id)
70+
member __.GetReplacements(documentId) = Seq.empty }
71+
}
72+
|> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)
73+
74+
type internal InlineRenameInfo
75+
(
76+
checker: FSharpChecker,
77+
projectInfoManager: ProjectInfoManager,
78+
document: Document,
79+
sourceText: SourceText,
80+
symbolUse: FSharpSymbolUse,
81+
declLoc: SymbolDeclarationLocation,
82+
checkFileResults: FSharpCheckFileResults
83+
) =
84+
85+
let getDocumentText (document: Document) cancellationToken =
86+
match document.TryGetText() with
87+
| true, text -> text
88+
| _ -> document.GetTextAsync(cancellationToken).Result
89+
90+
let triggerSpan =
91+
let span = CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate)
92+
CommonHelpers.fixupSpan(sourceText, span)
93+
94+
let symbolUses =
95+
async {
96+
let! symbolUses =
97+
match declLoc with
98+
| SymbolDeclarationLocation.CurrentDocument ->
99+
checkFileResults.GetUsesOfSymbolInFile(symbolUse.Symbol)
100+
| SymbolDeclarationLocation.Projects (projects, isInternalToProject) ->
101+
let projects =
102+
if isInternalToProject then projects
103+
else
104+
[ for project in projects do
105+
yield project
106+
yield! project.GetDependentProjects() ]
107+
|> List.distinctBy (fun x -> x.Id)
108+
109+
projects
110+
|> Seq.map (fun project ->
111+
async {
112+
match projectInfoManager.TryGetOptionsForProject(project.Id) with
113+
| Some options ->
114+
let! projectCheckResults = checker.ParseAndCheckProject(options)
115+
return! projectCheckResults.GetUsesOfSymbol(symbolUse.Symbol)
116+
| None -> return [||]
117+
})
118+
|> Async.Parallel
119+
|> Async.Map Array.concat
120+
121+
return
122+
(symbolUses
123+
|> Seq.collect (fun symbolUse ->
124+
document.Project.Solution.GetDocumentIdsWithFilePath(symbolUse.FileName) |> Seq.map (fun id -> id, symbolUse))
125+
|> Seq.groupBy fst
126+
).ToImmutableDictionary(
127+
(fun (id, _) -> id),
128+
fun (_, xs) -> xs |> Seq.map snd |> Seq.toArray)
129+
} |> Async.Cache
130+
131+
interface IInlineRenameInfo with
132+
member __.CanRename = true
133+
member __.LocalizedErrorMessage = null
134+
member __.TriggerSpan = triggerSpan
135+
member __.HasOverloads = false
136+
member __.ForceRenameOverloads = true
137+
member __.DisplayName = symbolUse.Symbol.DisplayName
138+
member __.FullDisplayName = try symbolUse.Symbol.FullName with _ -> symbolUse.Symbol.DisplayName
139+
member __.Glyph = Glyph.MethodPublic
140+
member __.GetFinalSymbolName replacementText = replacementText
141+
142+
member __.GetReferenceEditSpan(location, cancellationToken) =
143+
let text = getDocumentText location.Document cancellationToken
144+
CommonHelpers.fixupSpan(text, location.TextSpan)
145+
146+
member __.GetConflictEditSpan(location, _replacementText, _cancellationToken) = Nullable(location.TextSpan)
147+
148+
member __.FindRenameLocationsAsync(_optionSet, cancellationToken) =
149+
async {
150+
let! symbolUsesByDocumentId = symbolUses
151+
let! locationsByDocument =
152+
symbolUsesByDocumentId
153+
|> Seq.map (fun (KeyValue(documentId, symbolUses)) ->
154+
async {
155+
let document = document.Project.Solution.GetDocument(documentId)
156+
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
157+
let locations =
158+
symbolUses
159+
|> Array.map (fun symbolUse ->
160+
let textSpan = CommonHelpers.fixupSpan(sourceText, CommonRoslynHelpers.FSharpRangeToTextSpan(sourceText, symbolUse.RangeAlternate))
161+
InlineRenameLocation(document, textSpan))
162+
return { Document = document; Locations = locations }
163+
})
164+
|> Async.Parallel
165+
return InlineRenameLocationSet(locationsByDocument, document.Project.Solution) :> IInlineRenameLocationSet
166+
} |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)
167+
168+
member __.TryOnBeforeGlobalSymbolRenamed(_workspace, _changedDocumentIDs, _replacementText) = true
169+
member __.TryOnAfterGlobalSymbolRenamed(_workspace, _changedDocumentIDs, _replacementText) = true
170+
171+
[<ExportLanguageService(typeof<IEditorInlineRenameService>, FSharpCommonConstants.FSharpLanguageName); Shared>]
172+
type internal InlineRenameService
173+
[<ImportingConstructor>]
174+
(
175+
projectInfoManager: ProjectInfoManager,
176+
checkerProvider: FSharpCheckerProvider,
177+
[<ImportMany>] _refactorNotifyServices: seq<IRefactorNotifyService>
178+
) =
179+
180+
static member GetInlineRenameInfo(checker: FSharpChecker, projectInfoManager: ProjectInfoManager, document: Document, sourceText: SourceText, position: int,
181+
defines: string list, options: FSharpProjectOptions, textVersionHash: int, cancellationToken: CancellationToken) : Async<IInlineRenameInfo> =
182+
async {
183+
let textLine = sourceText.Lines.GetLineFromPosition(position)
184+
let textLinePos = sourceText.Lines.GetLinePosition(position)
185+
let fcsTextLineNumber = textLinePos.Line + 1 // Roslyn line numbers are zero-based, FSharp.Compiler.Service line numbers are 1-based
186+
187+
match CommonHelpers.tryClassifyAtPosition(document.Id, sourceText, document.FilePath, defines, position, cancellationToken) with
188+
| Some (islandColumn, qualifiers, _) ->
189+
let! _parseResults, checkFileAnswer = checker.ParseAndCheckFileInProject(document.FilePath, textVersionHash, sourceText.ToString(), options)
190+
191+
match checkFileAnswer with
192+
| FSharpCheckFileAnswer.Aborted -> return FailureInlineRenameInfo.Instance :> _
193+
| FSharpCheckFileAnswer.Succeeded(checkFileResults) ->
194+
195+
let! symbolUse = checkFileResults.GetSymbolUseAtLocation(fcsTextLineNumber, islandColumn, textLine.Text.ToString(), qualifiers)
196+
197+
match symbolUse with
198+
| Some symbolUse ->
199+
match symbolUse.GetDeclarationLocation(document) with
200+
| Some declLoc -> return InlineRenameInfo(checker, projectInfoManager, document, sourceText, symbolUse, declLoc, checkFileResults) :> _
201+
| _ -> return FailureInlineRenameInfo.Instance :> _
202+
| _ -> return FailureInlineRenameInfo.Instance :> _
203+
| None -> return FailureInlineRenameInfo.Instance :> _
204+
}
205+
206+
interface IEditorInlineRenameService with
207+
member __.GetRenameInfoAsync(document: Document, position: int, cancellationToken: CancellationToken) : Task<IInlineRenameInfo> =
208+
async {
209+
match projectInfoManager.TryGetOptionsForEditingDocumentOrProject(document) with
210+
| Some options ->
211+
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
212+
let! textVersion = document.GetTextVersionAsync(cancellationToken) |> Async.AwaitTask
213+
let defines = CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, options.OtherOptions |> Seq.toList)
214+
return! InlineRenameService.GetInlineRenameInfo(checkerProvider.Checker, projectInfoManager, document, sourceText, position, defines, options, textVersion.GetHashCode(), cancellationToken)
215+
| None -> return FailureInlineRenameInfo.Instance :> _
216+
}
217+
|> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)

0 commit comments

Comments
 (0)