How to implement a new snippet class
Lepiter can be extended with new types of snippets. This tutorial introduces a running example of a minimal “Block Quote” snippet that displays literal text (just like a Markdown block quote) to illustrate exactly the steps needed to create the new snippet type.
There is frequently a need to insert plain quoted text for code, but this is not well-supported by existing snippets. In Lepiter, the Markdown “block quote” notation introduces an executable code snippet, like this:
3+4
If we don't want executable code, we can set the “language” of the content to text
, like this:
3+4
This removes the evaluation buttons, but is not very nicely rendered. Instead we would like a new kind of editable “Block quote” snippet that looks like this:
3+4
How do we implement such a snippet?
A snippet implementation consists of three collaborating classes, a model , a view and a view model , following the MVVM pattern — Model–view–viewmodel. The model represents the domain model, holding the state of the snippet and all behavior related to the state. The view model holds and manages the state that is needed for the visual representation of the snippet in a page. Finally, the view is the element that provides the interactive display of the snippet. It is updated when the view model updates, and conversely, UI events that update the view are propagated to the view model.
Snippet model classes are all direct or indirect subclasses of LeSnippet
. In this case, a LeBlockQuoteSnippet
is a particular kind of
textual
snippet, so we define it as a subclass of LePlainTextSnippet
, an abstract superclass of pure textual (not code) snippets. As a model class, LeBlockQuoteSnippet
holds the actual content of the literal text block, as illustrated in this diagram at the left.

Snippet view model classes are all subclasses of LeSnippetViewModel
. In this case, LeBlockQuoteSnippetViewModel
subclasses LePlainTextCoderSnippetViewModel
and tracks the state of the text within a separate textCoderViewModel
.
Snippet views all subclass LeSnippetElement
. In this case, LeBlockQuoteSnippetElement
subclasses LePlainTextCoderSnippetElement
, which provides a basic editorElement
for editing text, an instance of GtTextualCoderEditorElement
.
Note that for a single given model, there may be multiple active views and view models. For example, the same page may be open in multiple windows. The snippets of that page each have a unique model, but there will be multiple views of those snippets, each with its own view model.
Note also that only snippet views that are actually visible on the screen are allocated, otherwise they may be recycled. The view models, on the other hand, will persist while the parent page is in view.
Now let's look at the details of each of these classes. We will describe the general steps, and the specific cases for our running example.
(1) Define a subclass of LeSnippet
.
Since literal block quotes are textual snippets, we define LeBlockQuoteSnippet
as a subclass of LePlainTextSnippet
, the parent of text-based snippets, such as LeTextSnippet
. Code snippets, on the other hand, subclass LeCodeSnippet
.
There are several methods to implement. We'll get to these step by step.
(2) Define and initialize any needed state. Define any accessors needed to recreate an instance with its state from plain strings.
We inherit all the state we need from LePlainTextSnippet
, namely a text
slot, which is initialized to an instance of BlRunRopedText
.
(LeBlockQuoteSnippet new string: 'hello') text
(3) Implement #storeOn:
to generate Smalltalk code which, when evaluated, creates a copy of this snippet.
We also inherit LeBlockQuoteSnippet>>#storeOn:
, which works out of the box for our new snippet:
(LeBlockQuoteSnippet new string: 'Literal text'; yourself) storeString
(4) Override the
class
methods leJsonV4AttributeMapping
and leJsonV4Name
to specify what state should be mapped to the JSON representation.
Snippets belong to Lepiter pages, which are (normally) stored in a Lepiter database as JSON files. (There are also transient databases that exist only in memory, but the more usual case if for databases to be persisted to the file system.) To support this, we must specify how to map the snippet to its JSON representation (and back).
We inherit LePlainTextSnippet>>#leJsonV4AttributeMapping
, and since we add no new state, this works out of the box.
On the other hand, we have to implement LeBlockQuoteSnippet>>#leJsonV4Name
to return a name — #blockQuoteSnippet
— for this kind of snippet in the string representation.
NB: To register the new mapping, we must update the singleton that stores all the mappings:
LeJsonV4 cleanUniqueInstance.
We can verify that a mapping now exists:
LeJsonV4 cleanUniqueInstance uniqueInstance newReader mappings includesKey: LeBlockQuoteSnippet
and we can serialize a block snippet like this:
LeJsonV4 uniqueInstance serializePretty: (LeBlockQuoteSnippet new string: 'hello'; yourself)
(5) Implement the boilerplate
class
methods contextMenuItemSpecification
and description
to include this snippet type in the contextual menu for Lepiter pages.
We implement LeBlockQuoteSnippet>>#contextMenuItemSpecification
and LeBlockQuoteSnippet>>#description
.
(6) Possibly implement canMoveToAnotherDatabase
to return true
if there are no dependencies to files in the database.
If a snippet has dependencies on files or attachments that are associated with the database of its page, then that page cannot be trivially moved to another database without breaking. If the necessary logic is not implement, this method should return false
.
The following classes currently return false
.
(LeSnippet allSubclasses reject: #isAbstract) select: [ :c | c new canMoveToAnotherDatabase not ]
We inherit this method from LeTextualSnippet>>#canMoveToAnotherDatabase
, and do not need to change it.
We do, however, need to add a test case of an example page containing the snippet to the example class MovingPagesExamples
for testing. We define MovingPagesExamples>>#blockQuoteSnippetPage
, making sure to set its method category to *GToolkit-Demo-Snippets
, so it will be an extension method from the new snippet's package, so as not to introduce a dependency from the Lepiter core package to our package. When we are done we can check that the tests pass, especially MovingPagesExamples>>#moveAllPages
.
(7) Implement required factory method empty
to return a basic instance.
Again, we inherit LeBlockQuoteSnippet>>#empty
, and do not need to change anything:
LeBlockQuoteSnippet empty
(8) Define the View Model class, and use it to implement the required method asSnippetViewModel
to return an instance of the view model holding all the state needed to create the view.
We define LeBlockQuoteSnippetViewModel
(see next section), and implement LeBlockQuoteSnippet>>#asSnippetViewModel
to return an instance of the view model initialized with this object (self
) as its snippet model.
(9) Implement acceptVisitor:
. This is needed for any utilities that visit the pages of a Lepiter database, such as the HTML export facility.
We implement LeBlockQuoteSnippet>>#acceptVisitor:
to simply apply visitTextSnippet:
. If we want to take special action, we will have to add a new visit
method, and possibly adapt the existing visitors. See the trait TLeModelVisitor
.
(10) Implement any remaining abstract methods.
The specific list will depend on where in the class hierarchy the snippet class is defined. We check for any remaining abstract methods as follows:
LeBlockQuoteSnippet allMethods select: #isAbstract
There are no remaining abstract methods left to implement, so we are done here.
(1) Define a subclass of LeSnippetViewModel
(or one of its subclasses).
We define LeBlockQuoteSnippetViewModel
as a subclass of LePlainTextCoderSnippetViewModel
. This class provides the scaffolding needed to subscribe to the snippet model, and to update the view model when there are changes.
(2) Define accessors for any state needed to maintain the view. Typically these will simply forward to the snippetModel
.
We inherit textCoder
and textCoderViewModel
, but we still have to initialize them.
We define LeBlockQuoteSnippetViewModel>>#initializeTextCoderViewModel
to define the text coder model and view model that we will need for the view.
(3) Implement the abstract method snippetElementClass
to return the class of the GUI element class for this snippet.
We implement LeBlockQuoteSnippetViewModel>>#snippetElementClass
to return the view class (next section).
(4) Implement any remaining abstract methods.
As before, we can check if any abstract methods remain to be implemented:
LeBlockQuoteSnippetViewModel allMethods select: #isAbstract
There is nothing left to implement, so we are done here.
(1) Define a subclass of LeSnippetElement
(or one of its subclasses).
We define LeBlockQuoteSnippetElement
as a subclass of LePlainTextCoderSnippetElement
, which serves as a base element for text-based snippets, and includes an instance of GtTextualCoderEditorElement
as its editorElement
.
(2) Define and initialize the snippet element.
We inherit and override LeBlockQuoteSnippetElement>>#initializeEditorElement
to set the editor elements background to a very light gray, and to use a code font.
(4) Implement any remaining abstract methods.
There is nothing left to do.
LeBlockQuoteSnippetElement allMethods select: #isAbstract
Now we are basically done. Don't forget, however, if we need to do anything special in visitors, we will have to change LeBlockQuoteSnippet>>#acceptVisitor:
, introduce a new visitBlockQuoteSnippet:
method for all visitors in the trait TLeModelVisitor
, and eventually update selected visitors to override the default implementation.