I’ve been busy rewriting the guts of Squeak’s Browser.
It’s an important tool: it’s the primary way we write code and explore the system. It’s also the tool I must use to edit the Browser. Time to operate on my own brain again!
The Browser is both a Model and a Controller, in terms of Model-View-Controller terminology. The View part varies a bit depending on what UI framework we use. In Morphic, the View will be objects like the SystemWindow, PluggableListMorphPlus instances for the lists, and so one. MVC the framework (as opposed to the pattern) uses different classes, of course, as in turn will Tweak (used in the Cobalt image, among other places).
But the Model doesn’t care. It uses ToolBuilder to abstract the particulars of your UI. In particular, ToolBuilder defines views in a fairly declarative manner:
buildMessageListWith: builder | listSpec | listSpec := builder pluggableListSpec new. listSpec model: self; list: #messageList; getIndex: #messageListIndex; setIndex: #messageListIndex:; menu: #messageListMenu:shifted:; keyPress: #messageListKey:from:. Preferences browseWithDragNDrop ifTrue:[listSpec dragItem: #dragFromMessageList:]. ^listSpec
You can see how ToolBuilder tells the UI how to call into the Model. Given a spec, ToolBuilder will then construct the View for you in whichever UI framework you use. (As usual, a lot more time goes into writing the framework than into writing documentation. The best place to see examples is in your Squeak image. A good place to start is looking at implementors of #buildWith:.)
In particular, when we implement #messageListIndex: we notify the View that something has changed, by calling
“self changed: #messageListIndex”. In particular, when we select a new method in the Browser we notify of #messageListIndex changing and #contents. That usually means calling Object>>changed:, which tells each of its dependents that the mentioned thing-that-changed has changed. That usually means calling Object>>changed:, which tells each of its dependents that the mentioned thing has changed. Each UI element’s #update: method checks the thing-that-changed, and eventually that means that the “self changed: #content” results in Browser>>content being called, which calculates what exactly we’ll see. (This is a general pattern in Squeak – the symbol representing the thing-that-changed is usually the name of the selector we use to act on that change.)
The Browser mostly consists of four lists. From left to right we have system categories (loosely speaking, this lists the packages (and subpackages) the image contains), classes (including whether one’s looking at the class or metaclass), message categories (sometimes referred to as protocols) and, finally, messages. All four lists share the same structure.
So why the rewrite?
First, the problem: up until now, a Browser remembered what category/class/method you were currently examining by storing the index of the selected item. Imagine you’re working in a message category that has the following methods, and you’re reading #baz:
You open another Browser because you’re halfway through reading #baz and don’t want to lose your place. In that browser, you quickly implement a method with name #add:. Now your original Browser knows you were reading the second method. That’s #ba… oh, no, it’s not #baz. It’s #bar. Oops! You just lost your place!
Solution? Remember the right thing. You don’t care that you’re reading the second method in some category. You care that you’re reading #baz. So instead of storing an index, store a name. Now you won’t lose your place!
Here’s the basic strategy: add a new instvar, selectedMessageName. Progressively add, in parallel to the existing references, references to selectedMessageName. Change direct references to messageListIndex to getter/setter calls. And then slowly extend the use of selectedMessageName, until nothing outside of #selectMessageNamed:, #selectedMessageName and #hasSelectedMessage reference the instvar. And now you can easily cut over to the new system. Done!
So how do you do the progressive rewrite itself? Choose a method. Write tests to thoroughly cover the method. (These tests are, obviously, not for the purpose of driving design; they’re documenting the existing behavior so that when we rewrite the method we know that we’ve refactored, and not changed the behaviour.) Then rewrite the method.
An aside: Squeak’s a highly interactive environment. As I’ve mentioned before, it’s quite normal for people to do most of their development work inside the debugger, much like a Lisper would use her REPL. So a quick and easy way to capture the existing behaviour of a method is to write a test that fails. When we run the test, we can inspect the object under test, and record its behaviour in the form of assertions. Leave a failing assertion, though – I usually use “self assert: false”. Hit the Proceed button, and the debugger will happily restart the test, and we can record the next piece of behaviour. Of course, this works best with stateless methods: another reason for us to try write our code in as functional a manner as we can!
Sometimes things aren’t quite so simple. Some methods do what they do purely through side effects. Sometimes it’s possible to return, say, a newly spawned Browser that we can then test (and close after the test completes!). Sometimes, well, we can’t. Try test them, make a note in the commit log asking people to try out that method (and to take care when doing so), and move on.
Now, again: we’re using the Browser to alter the Browser. If you mess up, it’s possible to put yourself in the position of being unable to use the very tool you need to fix. So, some tips:
- Save your work fairly often.
- Keep your commits small.
- Occasionally, when you’re sure your image is stable, save the entire image (as a new version).
- If you’re halfway through something you simply cannot commit, file out the current changeset.
- Lastly, keep your Monticello Browser open. MC’s various browsers don’t descend from Browser, so they will continue working even when you’ve messed up. In particular, that means you can roll back to a previous version by reloading the last version you saved. Nice!
As a last point, as we write tests we often discover long-lurking bugs!
So far I’ve replaced three of the four index instvars, namely the System Category, Class and Message list indices.
The messageListIndex is particularly interesting. It’s not just used in Browser, but also in Browser’s subclasses. (Don’t think of is-a relationships; I suspect that these browsers subclass Browser simply as an expedient code-reuse-through-inheritance.) As we saw above, we don’t care that we’re viewing the second method in the list, we care that we’re viewing a particular method. But in a MessageSet we might be viewing all implementors of a message – say #foo – in which case we don’t want to store the fact that we’re currently viewing #foo because all the messages have that name! At my present state of refactoring, I have kept the messageListIndex – “we’re viewing message #3” – in MessageSet, but will move to a more generic “we’re viewing this thing” where “thing” might be a message name or just as easily a MethodReference.