Writing Projections in TypeScript
The editor is always an implementation of the interface
PiProjection
. The
implementation generated by Freon is located in the file ~/picode/editor/gen/<yourLanguageName>ProjectionDefault.ts
.
It holds projections for all concepts in the language. When a projection is given in the .edit
file this is the one that will be in
the implementation, when no projection is defined, a default projection will be generated.
As you can read in the Freon Editor Framework , all projections are based on boxes. In the next few steps we will show you how to build a hierarchy of boxes to project your AST nodes, and how to style these boxes according to your wishes.
The projections in this section are available in the Freon-example.
Customize by implementing getBox()
In order to customize the editor you need to implement the getBox(...)
method in the
file ~/picode/editor/Custom<yourLanguageName>Projection.ts
.
For every concept that needs a customized projection, it should
return a Box
object. For all other concepts it should simply return null
.
This way Freon will know that you did not define a projection yourself and will use the projection
defined in ~/picode/editor/gen/<yourLanguageName>ProjectionDefault
.
// tutorial-language/editor/CustomEntityProjection.ts#L40-L43
getBox(element: PiElement): Box {
// Add any handmade projections of your own before next statement
return null;
}
How to Create a Box Object
Step 1 - Projecting a Simple Property
We start with building the projection for a simple property of type identifier
:
the name of the unit in our Entity language. In the metamodel this is represented by the value of
the property name
of class EntityModelUnit
.
// tutorial-language/defs/LanguageDefinition.ast#L56-L61
modelunit EntityModelUnit {
name: identifier;
functions: EntityFunction[];
entities: Entity[];
}
A reasonable choice for the projection of this property is a
HorizontalListBox
which holds a LabelBox
with
the name of the class, followed by the value stored in the variable name.
The following code shows a method that returns
this HorizontalListBox
. This method should be called in the overall method getBox(...)
.
// tutorial-language/editor/CustomEntityProjection.ts#L45-L51
// Most simple model box
private createModelBox(model: EntityModelUnit): Box {
return new HorizontalListBox(model, "model", [
new LabelBox(model, "model-label", "Model"),
new TextBox(model, "model-name", () => model.name, (c: string) => (model.name = c))
]);
}
When we start the editor based on this projection, we see the following:
Step 2 - Adding Style and a PlaceHolder
In version 0.5.0 this has been changed such that you can use CSS classes. More info follows…
Step 3 - Projecting a List
Next, we will add the entities
property of the EntityModelUnit
to the projection.
The entities
property is a list of Entity
.
// tutorial-language/defs/LanguageDefinition.ast#L60-L60
entities: Entity[];
In the projection we add a LabelBox
, to be shown
before the list, and the list itself using a VerticalListBox
to make sure that this list is
displayed vertically. Note that the LabelBox
is styled as a keyword, like the LabelBox
in the previous step.
// tutorial-language/editor/CustomEntityProjection.ts#L67-L86
return new VerticalListBox(model, "model", [
new HorizontalListBox(model, "model-info", [
new LabelBox(model, "model-keyword", "Model", {
style: styleToCSS(projectitStyles.keyword)
}),
new TextBox(model, "model-name", () => model.name, (c: string) => (model.name = c), {
placeHolder: "<name>"
})
]),
new LabelBox(model, "entity-keyword", "Entities", {
style: styleToCSS(projectitStyles.keyword)
}),
new VerticalListBox(
model,
"entity-list",
model.entities.map(ent => {
return this.rootProjection.getBox(ent);
})
)
]);
The projection of a single Entity
is done using this.rootProjection.getBox(ent)
. This will call a
separate function (here called createEntityBox
) that also returns a Box
, thus building a hierarchy of boxes. The use of
this.rootProjection.getBox(ent)
, instead of directly calling createEntityBox
,
ensures that the proper projection for entity is used, following the rules laid down in
customize projections.
We can track the hierarchy of boxes. First, have a look at the projection for Entity
, which is defines as follows in the .ast.
// tutorial-language/defs/LanguageDefinition.ast#L26-L32
concept Entity implements Type {
isCompany: boolean;
attributes: AttributeWithLimitedType[];
entAttributes: AttributeWithEntityType[];
functions: EntityFunction[];
reference baseEntity?: Entity;
}
Its projection is very similar to the projection of the EntityModel
,
showing the keyword Entity followed by its name and below all properties of the
entity in a VerticalListBox
.
// tutorial-language/editor/CustomEntityProjection.ts#L108-L127
private createEntityBox(entity: Entity): Box {
return new VerticalListBox(entity,"entity",
[
new HorizontalListBox(entity, "entity-info", [
new LabelBox(entity, "entity-keyword", "Entity", {
style: styleToCSS(projectitStyles.keyword)
}),
new TextBox(entity, "entity-name",
() => entity.name,
(c: string) => (entity.name = c),
{ placeHolder: "<name>" })
]),
new VerticalListBox( entity, "attribute-list",
entity.attributes.map(att => {
return this.rootProjection.getBox(att);
})
)
]
);
}
Next in the hierarchy of boxes is the projection of the elements of the attributes
list. Once again,
this projection is defined in its own function. Have a look at the .ast definition and the projection method.
Here, we use a HorizontalListBox
to show the property name, followed by a colon,
followed by its type.
// tutorial-language/defs/LanguageDefinition.ast#L71-L74
concept AttributeWithLimitedType {
reference declaredType: AttributeType;
name: identifier;
}
// tutorial-language/defs/LanguageDefinition.ast#L56-L61
modelunit EntityModelUnit {
name: identifier;
functions: EntityFunction[];
entities: Entity[];
}
// tutorial-language/editor/CustomEntityProjection.ts#L151-L188
public getAttributeBox(attribute: AttributeWithLimitedType): Box {
return new HorizontalListBox( // tag::AttributeBox[]
attribute,
"Attribute",
[
new TextBox(
attribute,"Attribute-name",
() => attribute.name,
(c: string) => (attribute.name = c as string),
),
new LabelBox(attribute, "attribute-label", ": "),
this.getReferenceBox(
attribute,
"Attribute-declaredType",
"<select declaredType>",
"AttributeType",
() => {
if (!!attribute.declaredType) {
return { id: attribute.declaredType.name, label: attribute.declaredType.name };
} else {
return null;
}
},
async (option: SelectOption): Promise<BehaviorExecutionResult> => {
if (!!option) {
attribute.declaredType = PiElementReference.create<AttributeType>(
EntityEnvironment.getInstance().scoper.getFromVisibleElements(attribute, option.label, "AttributeType") as AttributeType,
"Type"
);
} else {
attribute.declaredType = null;
}
return BehaviorExecutionResult.EXECUTED;
}
)
]
); // end::AttributeBox[]
}
Step 4 - Adding Behavior
The projection so far is exactly that: a projection. There are no actions defined yet, which we need to enable the user to change the model and add elements to it. However, we do have the built-in default behavior of the editor:
- Inside a TextBox the text can be edited.
- Using the arrow keys the user can navigate the projection.
- Using Ctrl-Arrow the user can navigate up and down the model/AST.
- When an element is selected, it can be deleted with the DEL key.
The default behavior takes care of changing simple AST nodes and deleting both simple and complex AST nodes. Find out more about adding behavior in Writing Actions.