How to perform an existing refactoring from another one
Refactor! Pro has numerous refactorings which perform a single task. What if we could run several refactorings at once?
For example, there is a support for optimizing namespace references added to the Move Type to File refactoring: after the new file is created, the Optimize Namespace References refactoring does its job. Here are other examples of refactoring (code provider) combinations, which can be performed:
- Introduce Local and Promote to Parameter will create a new refactoring, called something like “Introduce Parameter”.
- Declare Class (Struct) and Declare Method (Property) will create a new multi-declare code provider, which will declare everything at once.
- Introduce Parameter Object and Move Type to File will add a new option for the first refactoring to create a new file.
- And so much more…
So how is it possible to perform an existing refactoring or a code provider from another one?
Let’s assume we would like to execute the Rename refactoring from inside your own refactoring. The Rename refactoring creates linked identifiers (green boxes) over all references and its declaration. The following tasks should be performed:
- Execute the Rename refactoring, to create linked identifiers
- Change linked identifiers’ text to something different
- Commit linked identifiers change
Bear in mind, that we need to handle the Undo correctly – it should work in a single step, when the original refactoring is called (which calls others). So, here is the code with comments on how this can be achieved from inside of your refactoring’s Apply event. The code is some kind of “trick”, and future versions of DXCore will handle this more intelligently.
CSharp code:
private void refactoringProvider1_Apply(object sender, ApplyContentEventArgs ea)
{
// Find the registered Rename refactoring provider...
RefactoringProviderBase renameRefactoring = CodeRush.Refactoring.Get("Rename");
if (renameRefactoring == null)
return;
// The following two lines of code are a workaround of some bug (fixed in the 10.2.5+ version)...
CodeRush.SmartTags.HidePopupMenu();
CodeRush.Refactoring.MenuDeactivated();
// Sync caret position and selection range if needed...
CodeRush.SmartTags.UpdateContext();
// See if the Rename is available at the current caret position...
if (renameRefactoring.IsAvailable)
{
// Create a compound action, to make the Undo work correctly...
ICompoundAction compoundAction = CodeRush.TextBuffers.NewMultiFileCompoundAction(refactoringProvider1.DisplayName, true);
try
{
// Set the IsNestedProvider property to indicate that we are going to call the refactoring from inside another one...
// Note that the IsNestedProvider property is hidden from Intellisense.
renameRefactoring.IsNestedProvider = true;
// Apply the Rename refactoring...
renameRefactoring.Execute();
// Change linked identifiers created by the Rename provider...
RenameSelectedIdentifier(ea.SelectionRange);
}
catch (Exception e)
{
throw e;
}
finally
{
// Restore the IsNestedProvider property to default value...
renameRefactoring.IsNestedProvider = false;
// Commint the Undo...
compoundAction.Close();
}
}
}
private static void RenameSelectedIdentifier(SourceRange selectionRange)
{
// See if we are inside of a linked identifier...
if (CodeRush.LinkedIdentifiers.Active)
{
ILinkedIdentifierStorage activeStorage = CodeRush.LinkedIdentifiers.ActiveStorage;
// Find a linked identifier at the caret position...
ILinkedIdentifier[] linkedIdentifiers = activeStorage.Find(selectionRange);
if (linkedIdentifiers != null && linkedIdentifiers.Length > 0)
{
ILinkedIdentifier linkedIdentifier = linkedIdentifiers[0];
// Change the text...
linkedIdentifier.Text += "Renamed";
// Select the linked identifier, to commit it...
CodeRush.TextViews.Active.Select(linkedIdentifier.Range);
// Commit the change...
CodeRush.Command.Execute("Break All Linked Identifiers");
}
}
}
Visual Basic code:
Private Sub RefactoringProvider1_Apply(sender As System.Object, ea As ApplyContentEventArgs) Handles RefactoringProvider1.Apply
' Find the registered Rename refactoring provider...
Dim renameRefactoring As RefactoringProviderBase = CodeRush.Refactoring.Get("Rename")
If renameRefactoring Is Nothing Then
Return
End If
' The following two lines of code are a workaround of some bug (fixed in the 10.2.5+ version)...
CodeRush.SmartTags.HidePopupMenu()
CodeRush.Refactoring.MenuDeactivated()
' Sync caret position and selection range if needed...
CodeRush.SmartTags.UpdateContext();
' See if the Rename is available at the current caret position...
If renameRefactoring.IsAvailable Then
' Create a compound action, to make the Undo work correctly...
Dim compoundAction As ICompoundAction = CodeRush.TextBuffers.NewMultiFileCompoundAction(RefactoringProvider1.DisplayName, True)
Try
' Set the IsNestedProvider property to indicate that we are going to call the refactoring from inside another one...
renameRefactoring.IsNestedProvider = True
' Apply the Rename refactoring...
renameRefactoring.Execute()
' Change linked identifiers created by the Rename provider...
RenameSelectedIdentifier(ea.SelectionRange)
Catch e As Exception
Throw e
Finally
' Restore the IsNestedProvider property to default value...
renameRefactoring.IsNestedProvider = False
' Commint the Undo...
compoundAction.Close()
End Try
End If
End Sub
Private Shared Sub RenameSelectedIdentifier(ByVal selectionRange As SourceRange)
' See if we are inside of a linked identifier...
If CodeRush.LinkedIdentifiers.Active Then
Dim activeStorage As ILinkedIdentifierStorage = CodeRush.LinkedIdentifiers.ActiveStorage
' Find a linked identifier at the caret position...
Dim linkedIdentifiers As ILinkedIdentifier() = activeStorage.Find(selectionRange)
If linkedIdentifiers IsNot Nothing AndAlso linkedIdentifiers.Length > 0 Then
Dim linkedIdentifier As ILinkedIdentifier = linkedIdentifiers(0)
' Change the text...
linkedIdentifier.Text += "Renamed"
' Select the linked identifier, to commit it...
CodeRush.TextViews.Active.Select(linkedIdentifier.Range)
' Commit the change...
CodeRush.Command.Execute("Break All Linked Identifiers")
End If
End If
End Sub
The plug-in source (VS2010) and binaries are attached.
—– Products: DXCore, Refactor! Versions: 10.2 and up VS IDEs: any Updated: Apr/16/2012 ID: D053
Hey! That looks great; I’ll give it a go this evening. Thanks again, and as always.
Hi Alex – I didn’t get a chance to try this code until this morning – AFTER installing 2010.2.4. The RefactoringProviderBase.IsNestedProvider property is not recognized; did this property go away (or hopefully just move somewhere else) in 2.4? I looked through Breaking Changes in the What’s New file, but no references there…
Hi Robert, please try “IsNestedRefactoring” instead.
Alex, RefactoringProviderBase doesn’t seem to have any members which contain “Nested”. Should we be casting renameRefactoring?
These methods are hidden from Intellisense, so your version of IDE tools should contain hidden IsNestedRefactoring method.
Success! Much thanks.
I’m trying to rename an variable that I find by navigating SourceFile.AllFiles().
file is my SourceFile and field is a Variable; then I do:
If file.Document Is Nothing Then
CodeRush.File.Activate(file.FilePath)
End If
CodeRush.Editor.Activate(DirectCast(file.Document, Document), True)
CodeRush.Caret.MoveTo(field.NameRange.Start, True)
CodeRush.Selection.SelectRange(field.NameRange)
followed by the rename handling code from above. But CodeRush.LinkedIdentifiers.Active is not true unless I manually put my caret on a field. The linkedIdentifiers.Length is also 0 unless I’ve manually put the caret on the field.
Any hints?
@Sebastian Andersson
I’ve tried a similar code – and it works correctly on my side. Can you share your source code for investigation? here or e-mail at alex@skorkin.com.
Sure, here it is. The “foundIt” code is only there to make it faster to test, the idea is to change all private fields at once (or perhaps per file or project to make the commits smaller).
Imports DevExpress.CodeRush.Core
Imports DevExpress.CodeRush.StructuralParser
Imports DevExpress.DXCore.TextBuffers
Public Class RenameHogiaNamedPrivateFields
‘DXCore-generated code…
#Region ” InitializePlugIn ”
Public Overrides Sub InitializePlugIn()
MyBase.InitializePlugIn()
‘TODO: Add your initialization code here.
End Sub
#End Region
#Region ” FinalizePlugIn ”
Public Overrides Sub FinalizePlugIn()
‘TODO: Add your finalization code here.
MyBase.FinalizePlugIn()
End Sub
#End Region
Private Sub actPerformeRename_Execute(ByVal ea As DevExpress.CodeRush.Core.ExecuteEventArgs) Handles actPerformeRename.Execute
Dim foundIt As Boolean = False
Dim renameRefactoring As RefactoringProviderBase = CodeRush.Refactoring.Get(“Rename”)
CodeRush.SmartTags.HidePopupMenu()
CodeRush.Refactoring.MenuDeactivated()
Dim compoundAction As ICompoundAction = CodeRush.TextBuffers.NewMultiFileCompoundAction(“Rename private fields”, True)
Try
For Each file As SourceFile In CodeRush.Source.ActiveSolution().AllFiles()
Debug.WriteLine(file.ToString() & “:”)
For Each type As ITypeElement In file.AllTypes()
Debug.WriteLine(” {0} : {1}”, type, type.GetType.FullName)
If TypeOf type Is DevExpress.CodeRush.StructuralParser.Class Then
Dim klass As DevExpress.CodeRush.StructuralParser.Class = type
For Each field As Variable In klass.AllVariables
If field.Visibility = MemberVisibility.Private Then
If field.Name.StartsWith(“iThe”) OrElse _
field.Name.StartsWith(“sThe”) Then
foundIt = True
Debug.WriteLine(” * {0} : {1}”, field, field.GetType.FullName)
If file.Document Is Nothing Then
CodeRush.File.Activate(file.FilePath)
End If
CodeRush.Editor.Activate(DirectCast(file.Document, Document), True)
CodeRush.Caret.MoveTo(field.NameRange.Start, True)
CodeRush.Selection.SelectRange(field.NameRange)
renameRefactoring.IsNestedRefactoring = True
Try
renameRefactoring.Execute()
RenameSelectedIdentifier(field.NameRange, field)
Finally
renameRefactoring.IsNestedRefactoring = False
End Try
End If
End If
Next
End If
Next
If foundIt Then Exit For
Next
Finally
compoundAction.Close()
End Try
End Sub
Private Shared Sub RenameSelectedIdentifier(ByVal selectionRange As SourceRange, ByVal field As Variable)
‘ See if we are inside of a linked identifier…
If CodeRush.LinkedIdentifiers.Active Then
Dim activeStorage As ILinkedIdentifierStorage = CodeRush.LinkedIdentifiers.ActiveStorage
‘ Find a linked identifier at the caret position…
Dim linkedIdentifiers As ILinkedIdentifier() = activeStorage.Find(selectionRange)
If linkedIdentifiers IsNot Nothing AndAlso linkedIdentifiers.Length > 0 Then
Dim linkedIdentifier As ILinkedIdentifier = linkedIdentifiers(0)
‘ Change the text…
linkedIdentifier.Text = String.Format(“_{0}{1}”, Char.ToLower(field.Name.Chars(4)), field.Name.Substring(4 + 1))
‘ Select the linked identifier, to commit it…
CodeRush.TextViews.Active.Select(linkedIdentifier.Range)
‘ Commit the change…
CodeRush.Command.Execute(“Break All Linked Identifiers”)
End If
End If
End Sub
End Class
The code is missing the important “renameRefactoring.IsAvailable” call, which tells DXCore to sync everything and check the availability of the refactoring. The most valuable issue with the code is that once rename is applied, the source tree gets out of sync, so, the “Find All References” engine cannot be performed and that’s why rename is not available – it cannot find references to a field. The sample in this post is intended to perform the rename refactoring once for a single item. If you’d like to rename several items, I would suggest using another way – I’ll write a post about it soon. For now, you can do this – collect ranges with the corresponding files and then apply rename using that list – the source tree will corrupt after the first rename operation, but the range of other fields in the list shouldn’t change. We’ll manually sync the parse tree by calling the TextDocument.ParserIfTextChanged. Here’s the code – replace your “actPerformeRename_Execute” method with the following code:
Private Sub actPerformeRename_Execute(ByVal ea As DevExpress.CodeRush.Core.ExecuteEventArgs) Handles Action1.Execute
Dim compoundAction As ICompoundAction = CodeRush.TextBuffers.NewMultiFileCompoundAction(“Rename private fields”, True)
Try
Dim listOfItemsToRename As New Dictionary(Of String, List(Of SourceRange))
For Each file As SourceFile In CodeRush.Source.ActiveSolution().AllFiles()
Dim listOfFieldNameRanges As New List(Of SourceRange)
Debug.WriteLine(file.ToString() & “:”)
For Each type As ITypeElement In file.AllTypes()
Debug.WriteLine(” {0} : {1}”, type, type.GetType.FullName)
If TypeOf type Is DevExpress.CodeRush.StructuralParser.Class Then
Dim klass As DevExpress.CodeRush.StructuralParser.Class = type
For Each field As Variable In klass.AllVariables
If field.Visibility = MemberVisibility.Private Then
If field.Name.StartsWith(“iThe”) OrElse _
field.Name.StartsWith(“sThe”) Then
Debug.WriteLine(” * {0} : {1}”, field, field.GetType.FullName)
listOfFieldNameRanges.Add(field.NameRange)
End If
End If
Next
End If
Next
If (listOfFieldNameRanges.Count > 0) Then
listOfItemsToRename.Add(file.Name, listOfFieldNameRanges)
End If
Next
RenameItems(listOfItemsToRename)
Finally
compoundAction.Close()
End Try
End Sub
Private Sub RenameItems(ByVal itemsToRename As Dictionary(Of String, List(Of SourceRange)))
Dim renameRefactoring As RefactoringProviderBase = CodeRush.Refactoring.Get(“Rename”)
For Each item As KeyValuePair(Of String, List(Of SourceRange)) In itemsToRename
Dim fileName As String = item.Key
Dim fieldNameRanges As List(Of SourceRange) = item.Value
CodeRush.File.Activate(fileName)
For Each fieldRange As SourceRange In fieldNameRanges
CodeRush.Documents.ActiveTextDocument.ParseIfTextChanged()
CodeRush.Selection.SelectRange(fieldRange)
CodeRush.SmartTags.UpdateContext()
If renameRefactoring.IsAvailable Then
renameRefactoring.IsNestedRefactoring = True
Try
renameRefactoring.Execute()
RenameSelectedIdentifier(fieldRange)
Finally
renameRefactoring.IsNestedRefactoring = False
End Try
End If
Next
Next
End Sub
Thanks!
That worked almost perfectly. There were three cases where only the field was renamed, not the references and two cases where the field was not renamed, but this was from hundreds of fields.
This probably saved me at least a week of work!
@Sebastian Andersson
Excellent! I’m glad to hear it helped you.