Home > Code Samples > How to perform an existing refactoring from another one

How to perform an existing refactoring from another one

December 25th, 2010

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

Similar Posts:

  1. Robert Leahey
    December 29th, 2010 at 11:23 | #1

    Hey! That looks great; I’ll give it a go this evening. Thanks again, and as always.

  2. Robert Leahey
    January 3rd, 2011 at 09:20 | #2

    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…

  3. January 3rd, 2011 at 14:03 | #3

    Hi Robert, please try “IsNestedRefactoring” instead.

  4. Robert Leahey
    January 3rd, 2011 at 14:19 | #4

    Alex, RefactoringProviderBase doesn’t seem to have any members which contain “Nested”. Should we be casting renameRefactoring?

  5. January 3rd, 2011 at 14:55 | #5

    These methods are hidden from Intellisense, so your version of IDE tools should contain hidden IsNestedRefactoring method.

  6. Robert Leahey
    January 5th, 2011 at 12:08 | #6

    Success! Much thanks.

  7. Sebastian Andersson
    March 2nd, 2011 at 03:04 | #7

    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?

  8. March 2nd, 2011 at 09:52 | #8

    @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.

  9. Sebastian Andersson
    March 2nd, 2011 at 23:34 | #9

    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

  10. March 3rd, 2011 at 14:48 | #10

    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

  11. Sebastian Andersson
    March 4th, 2011 at 01:09 | #11

    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!

  12. March 4th, 2011 at 03:09 | #12

    @Sebastian Andersson
    Excellent! I’m glad to hear it helped you.