- Ko, A. J. and Myers, B. A. (2005). Citrus: A Language and Toolkit for Simplifying the Creation of Structured Editors for Code and Data. UIST 2005, Seattle WA, October 23-26, 2005, 3-12.
[ local ] [ acm ]
- Watch the Citrus video (MPEG4, 28 MB, requires Quicktime 6 or up)
- Look for a download soon. If you want to be notified of when it's posted, e-mail Andy and he'll add you to the list.
Graphical structured editors for code and data have many benefits over editing raw XML, but they can be difficult and time-consuming to build using modern programming languages. Citrus is a new object-oriented, interpreted language that is designed to simplify the creation of such editors, by providing first-class language support for one-way constraints, custom events and event handlers, and value restrictions and validation. We're using Citrus to implement several novel software development tools.
Fortunately, these editors have many of the same architectural requirements:
- Model-view-controller patterns (multiple views)
- Listener patterns (event notification)
- Documents are typically well-defined trees & DAGs
- Incrementally validate syntax & semantics
- Provide incremental feedback about edits
- Maintain constraints among data items
We designed Citrus, a new programming language, to support these these architectural patterns as first class language constructs. Citrus is object-oriented, interpreted, reflective, statically-typed, and polymorphic.
objects and properties
Just like any other object-oriented language, at runtime Citrus programs consist of many objects, which have several fields that are pointers to other objects. This pointers are a single value, representing the location of an object in memory. In Citrus, these pointers are objects themselves. We call them properties. Properties have lots of additional meta-information in addition to their value, including listeners, restrictions, constraints, owners, and other information.
One unique feature of properties is that they either own or refer to their value. This was driven by the observation that the data in structured editors frequently represent trees or DAGs. Consequently, programmers frequently need to write code to determine what owns or contains an object (for example, a view's parent or a statement's block in an AST). They also frequently need to determine what references an object (for example, a method's callers or a model's views). To access this data, programmers have to implement custom functions or manually maintain references.
To minimize this work, in Citrus, properties have back-pointers to the object that owns them, and objects have a set of properties that point to them. Each object thus has a single property that owns it and a set of properties that refer to it. All of these backpointers are managed by the runtime automatically.
Let's look at some examples of where these backpointers become useful. The first example shows a label in a button. To programatically access the button from the label, we only need a single line of code
# The button that contains this label
The second example shows an object in a graphical editor. We only need a single call to the ownerOfType method to look up the hierarchy of owners to find the ScrollView that contains the square.
# The ScrollView that contains this circle.
(circle ownerOfType ScrollView)
The third example shows a model and its three views. We can access all of the views that reference the model with this single call to the refsOfType method.
# Views of this object
(model refsOfType View)
To find all of Identifiers in the abstract syntax tree that reference the method, for example, as part of renaming the method, we only need this single call to refsOfType. ‰χέ
# All Identifiers that refer to this method
(method refsOfType Identifier)
The central way to maintain relationships in Citrus is through constraints. Constraints are one-way, like the constraints in Amulet and Subarctic toolkits, but arbitrary code can be used, and dependencies are gathered automatically instead of having to explicitly declare them.
In this example, the dependencies include all properties accessed inside the previousView function, and the right property.