In this article I describe a simple add-on system, initially developed for Suneido’s code editor (Scintilla), but likely useful for other things as well.
It makes it easy to write functionality as separate add-on classes, rather than ending up with one huge class.
The library record for ScintillaControl (the code editing component Suneido uses) has gotten pretty large. With the improvements that Santiago and I have done lately it was getting even larger. In an attempt to improve this I split it into several parts.
Ideally, I like to keep the Suneido “wrapper” around a Windows control as “thin” as possible – just an interface. So I moved everything except the basic interface out of ScintillaControl into ScintillaLexControl that added code styling, and ScintillaCodeControl that added featurs like auto-completion. These were connected by inheritance i.e. ScintillaCode inherited from ScintillaLex which inherited from Scintilla.
But then I wanted to improve the QueryView editor. I wanted some, but not all, of the features from ScintillaCode. And I had to modify some of the features like auto-completion to work with queries. It was hard to fit this into my inheritance hierarchy.
When object-oriented programming was new, we thought inheritance was the answer to everything. But over the years it’s become clear that it’s not the answer to every problem, and that it is commonly abused. In many situations, composition and delegation can be better solutions.
I decided a better approach would be to write each of the editing features as a separate “add-on” class and have a way to create a desired editor by choosing the appropriate set of add-ons.
Suneido has an existing “plug-in” system, but it didn’t really fit with what was needed, so I decided to write a new add-on system. Initially I was just going to make it part of Scintilla but then I realized it could be kept separate so it could be used for other things. (And the code was cleaner that way.)
Here is Addon_highlight_words (it highlights all the occurrences of the word under the cursor)
Addon { indic_word: 11 UpdateUI() { .ClearIndicator(.indic_word) wordChars = ScintillaLexControl.WordChars org = end = .GetCurrentPos() while wordChars.Has?(.GetAt(org - 1)) --org while wordChars.Has?(.GetAt(end)) ++end if org < end .mark_all_occurences(.GetRange(org, end)) } mark_all_occurences(word) { if not word.Identifier?() return n = 0 text = .Get() pat = "\<(?q)" $ word $ "(?-q)\>" while false isnt m = text.Match(pat, prev:) { m = m[0] .SetIndicator(.indic_word, m[0], m[1]) text = text.Substr(0, m[0]) ++n } if n < 2 .ClearIndicator(.indic_word) } }
This is almost identical to the code that was in ScintillaCodeControl.
Here is the base class for an add-on (called Addon)
class { New(parent) { .parent = parent } Default(@args) { .parent[args[0]](@+1 args) } }
As you can see, it does very little. The “parent” is the object that we’re adding on to, e.g. the Scintilla control. The Default method automatically delegates method calls to the parent. This isn’t strictly necessary, but it allows you to write the add-ons methods as if they were in the parent class.
To connect Scintilla to the add-on system is pretty simple. Rather than risk “breaking” Scintilla while I was testing, I wrote a new ScintillaAddonsControl:
ScintillaLexControl { New(@args) { super(@args) .Map[SCN.UPDATEUI] = 'UPDATEUI' .addons = AddonManager(this, .Base(), args) } UPDATEUI() { .addons.Send(#UpdateUI) return 0 } }
It simply constructs an AddonManager, passing it a list of objects to look for add-ons in, and then uses the manager to pass events to the add-ons. The add-on system doesn’t predefine the set of events you can use, you just have to make sure that whatever events you send are handled by the add-ons. You might want to define a base class for your add-ons with empty versions of the event handlers so individual add-ons only have to deal with the events they are interested in.
And here is AddonManager:
class { New(@args) { .parent = args[0] .addons = Object() for (ob in args) .getAddonsFrom(ob) } getAddonsFrom(ob) { for m in ob.Members() if m.Prefix?('Addon_') .construct(m) } construct(name) { c = name.Eval() addon = c(.parent) // make an instance .addons.Add(addon) } Send(@args) { for addon in .addons addon[args[0]](@+1 args) } }
All that is left is to connect ScintillaAddonsControl to the desired add-ons. This can be done in a couple of ways.
By passing the add-ons as arguments to the control:
ScintillaAddonsControl(Addon_highlight_words:)
As a “control specification” in a user interface layout this would look like:
(ScintillaAddons Addon_highlight_words)
Or we could define a new sub-class with a particular set of add-ons:
ScintillaAddons { Addon_highlight_words: }
Now I just have to move most of the Scintilla code into add-ons.
The code above is an early, slightly simplified version. Look for a more complete version in future releases.
Some possible enhancements:
- allow adding add-ons dynamically
- allow passing options to the add-ons
- allow multiple copies of the same add-on with different options
- exception handling
- a user interface so users can enable/disable/configure add-ons
- check that add-ons are intended for where they are used