Last year Jake Wharton published an article titled Just Say mNo to Hungarian Notation and I published a blog post showing how to flag Hungarian Notation as an error in Android Studio. It caused a little bit of controversy.
Just to be clear, it was never my intention for it to be a "you must get rid of Hungarian Notation" post. It just seemed like a good topic to use to show the power of structural search and inspections in Android Studio/IntelliJ.
Bearing that in mind, here is a way to use Android Studio's structural search and replace to replace all of your Hungarian Notations. :-)
(It's probably worth a quick read of how to flag Hungarian Notation as an error in Android Studio before continuing with this one.)
Before we start there's a plugin that will really help with structural search and replace.
Install the plugin called PsiViewer
. PSI stands for Program Structure Interface and
you can learn more about it at http://jetbrains.org/intellij/sdk/docs/basics/architectural_overview/psi_files.html
In a short it's the way that Android Studio/IntelliJ internally represents the code that you are writing. For what we
are about to attempt we need to peek into the PSI. This plugin
allows you to inspect the structure of the PSI tree for the code you are writing. So go ahead and install
it. It's ok, I'll wait while you do that and restart Android Studio.
Time passes...
All done? Cool. We'll come back to it in a little while when we need it. For now let's start off with a very simple, contrived app with two fields that use Hungarian Notation.
In this example code there are two fields that have
Hungarian Notation (one 'm' and one 's'). There is also a method that looks similar -
mDummy()
. For this increasingly contrived example we want to leave mDummy()
alone
and just change the fields.
The first thing we are going to do is use the structural search to narrow down what we want to replace later. Use the Find Action shortcut (Shift-Command-A for Mac, Ctrl-Shift-A for Linux/Windows) and type in "Search Structurally"
This will bring up the Strutural Search dialog. In the Search Template enter $Symbol$
This is basically a variable
that will hold the search matches.
Hit the button Edit variables and select the Symbol
variable from the list. In the Text/regexp fields
enter (m|s)[A-Z].*
This regular expression will match any string that starts with a lower case 'm' or 's' followed by
a capital letter (seem familiar?).
Hit OK. Check that the Scope of the search is set to Module 'app'
and hit the Find button and examine the search results at the bottom. Ah. There's a problem. As well as finding our
two fields and the occurrences of them in the code, it has also found the method name mDummy()
. How do we avoid that?
This is where PSI comes in. Show the PsiViewer tool window by clicking on the PsiViewer button on the side of the window, using the menu View -> Tool Windows -> PsiViewer or by using the Find Action command and entering PsiViewer. The PsiViewer tool window will show the PSI tree for the code. As you move your edit cursor around your code the element where the cursor is gets highlighted in the PsiViewer.
Move the cursor up to the mFmt
field definition at the top of the class. The PsiViewer shows this as PsiIdentifier:mFmt
and in the
properties below it shows the class as com.intellij.psi.impl.source.tree.java.PsiIdentifierImpl
. It just trips off
the tongue doesn't it? It's the same class for the sAnotherFormat
field. Can we make use of this?
Of course we can. Go back to Search Structurally, click on Edit Variables and select Symbol. The regular expression is still set. At the bottom of the dialog there is a section called Script constraints. Click on the three dots next to Script text and enter
Symbol instanceof com.intellij.psi.impl.source.tree.java.PsiIdentifierImpl
It should look a little like this:
What happens is when Android Studio is doing a search it runs this piece of groovy against the possible matches it finds, and if it evaluates/returns true then it is a match. So the above means that it will only match things that match the regular expression and if they are of the correct Psi class. Let's give it a try. Click on OK, OK and Find. Drum roll....
Oh. Nothing is found.
The reason is the Psi symbol that is matched isn't actually the PsiIdentifier, it's the PsiField that it is part of a little way up the tree.
So go back to the structural search, edit variables and edit the script constraint to read
Symbol instanceof com.intellij.psi.impl.source.PsiFieldImpl
So now it looks like:
This time when we perform the search we get something. But there's still a problem. Although it isn't matching mDummy()
any more it has only found the field definitions, not the
usages in the code. We've gone from finding too much to not finding enough. Sigh
Try selecting the usages of the mFmt
and sAnotherFormat
in the code and they will show in the PsiViewer as PsiIdentifier:mFmt
.
Hmm...we've been here before. Let's walk up the tree a branch and we see PsiReferenceExpression:mFmt
which has
a class of com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl
. Let's use it.
Update the script constraint to
Symbol instanceof com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl ||
Symbol instanceof com.intellij.psi.impl.source.PsiFieldImpl
So it now looks like:
So now this will return true if the Psi element is either of these types. Doing the find now finds all of the instances we want to change and none of the ones we don't. Yippee!
How do we replace these variables with non-Hungarian versions? We use Replace Structurally of course. Use the Find Actions...
erm...action to go to Replace Structurally. The search template should be the same as it was from the Search
Structurally so we can leave that alone, but feel free to check it to be sure. In the Replacement template part
enter $UpdatedSymbol$
. Again like $Symbol$
this is just a variable. We are telling Android Studio that we want
to replace whatever it assigns to $Symbol$
with $UpdatedSymbol$
. So we need to tell it what we want that
to be. Hit Edit variables and in the Edit Variables dialog click on UpdatedSymbol.
Another script. This time what we return from the script is what we want the replacement text to be. Here's one I prepared earlier for you to copy and paste into the script text. (Click on the three dots to get a text edit dialog.)
def vname
if (Symbol instanceof com.intellij.psi.impl.source.tree.java.PsiReferenceExpressionImpl) {
vname = Symbol.referenceName
} else {
vname = Symbol.name
}
return vname.substring(1,2).toLowerCase() + vname.substring(2)
What is this doing? Well the problem is that we can match on two different classes. For the PsiReferenceExpressionImpl
class the field that contains the variable name is...well there are a few so I chose to use referenceName
. However for
the PsiFieldImp
class the field we need is called name
. So the first part gets the appropriate string based on the
class that we have found and sticks it in vname
. The last line removes the first letter, lowercases the second letter,
and then appends the rest of the name.
(Note that in the script for $Symbol$
we don't use return
but we do in this one. In actual fact we don't need the
return
here either.
Groovy automatically returns the value of the last expression evaluated, and if you press
an ALT-Return with the cursor on the return
in the script dialogs the quick fix suggestion that comes up is
Remove 'return' keyword. But it's useful to know as you could write some pretty complicated scripts in there and they
might be clearer with return statements in, so I thought I'd show this one with it. But I digress...)
Hit OK a couple of times and then do the Find and in the results at the bottom you'll see, well, not much change except the addition of three buttons.
If you're feeling brave you can do Replace All. If you're not so brave you can do Replace Selected for each result.
You can also select one of the results and click on Preview Replacement. If you do that for one of the lines with
mFmt
the preview will show fmt
and for sAnotherFormat
it will show anotherFormat
.
Live dangerously and hit Replace All and examine the new code. It's a thing of beauty isn't it?
You might need to experiment with the PsiViewer. There may be other types that you need to match against. For
example if you were to import a static reference to a field that gets updated by the replace the import statement won't get updated with
the matches listed above.; you would need to expand the scripts to include elements of class
com.intellij.psi.impl.source.PsiImportStaticReferenceImpl
. You will have to experiment to find any other PSI classes
that need to be included.
Something that is noteworthy is that structural searching and replacing is not limited to Java files. Try opening up a layout XML file and looking at the PsiViewer. There are PSI classes for XML, so you could create a search/replace for specific elements in your layouts, strings, dimens, etc.
You have the tools and you have the talent, and now the world is your mLobster.
Darren @ Æ
Comments
comments powered by Disqus