Gramme - Model
Gramme is a grammar driven modal editor.
Part 1 covered the motivation for Gramme. This post will dive into the model underlying how Gramme works.
Following in the tradition of Vim, it uses the concept of modes to determine how to process user input. Pressing the /
key in insert mode will insert a “/” character wherever the cursor is in the current buffer. In command mode, the /
key signals the start of a search, allowing the user to search for any literal string in the current buffer. Other modes can further specify how /
should be interpreted.
The distinguishing feature of Gramme is that grammars form the foundation of how it behaves, both at the user and implementation level. The hope is that by unifying these two abstraction layers, users who understand how to use Gramme will also gain some intuition in how it operates and how it’s implemented, making it easier to extend the system.
One of the primary goals of Gramme is to provide a gentler learning curve than Vim, with minimal compromise to the flexibility a command system like Vim’s provides. To accomplish this, Gramme’s command system is formally defined using a relatively small number of rules, relying on composition to build up commands from smaller, reusable components. The result is that the behavior of the system is, overall, more consistent and predictable, and so is also easier to both explore and extend. The key to all of these benefits is the foundation of grammars.
Grammars
In natural languages, a grammar is a system of rules about how components (words, phrases, etc) may be composed to form complete expressions. In Gramme, grammars define how commands are created from smaller, reusable components.
Words are the basic building blocks for defining behavior in Gramme. Abstractly, they represent some re-usable behavior that a user may wish to execute in the editor. Words can be combined into phrases, such that the words in a phrase modify the behavior of one another. Put another way, phrases which share a word should share some kind of behavior, and the differences in their behavior should be relatable through the differences in their words. Concretely, a word is simply a function (in the classic functional programming sense) that the editor is aware of and exposes to the user. By extension, a phrase is just a composition of functions.
A fundamental characteristic of words is that they may be combined and composed into phrases. The other words in the phrase narrow down the base behavior the words possess individually. Just like in natural languages, words may be swapped out in a phrase to produce a similar phrase with a slightly different meaning. Within a phrase, the part of speech (PoS) of a word defines that word’s function within the phrase. Words that may have the same part of speech can be swapped for one another in a phrase to produce a new, meaningful phrase. Though each distinct phrase has a slightly different meaning, those differences should be predictable based on which words are different.
Grammars abstractly define how words can be composed, in terms of parts of speech. They define the valid structures or forms that user input can take which are meaningful to Gramme. When a phrase completes a grammar by providing words for each part of speech, Gramme can produce a valid, executable command by composing together the constituent functions associated with those words. Note that while these commands are executable, this does not guarantee that they produce meaningful results. For example, pressing the backspace
key in insert mode in an empty buffer represents a valid command, but that command does not produce a meaningful result.
Lastly, Gramme provides a system of bindings which associate commonly used words to to single characters for efficient input, and taxonomies for organizing words into hierarchies for easier discovery.
Example
This section will walk through a short example illustrating how a short snippet of user input relates to each of the concepts described above. All of the bindings discussed below refer to the default bindings provided by Gramme. If a binding is less frequently used than another, the user can override the binding.
Consider the following phrase:
ciw
In command mode, this input translates to a command to delete the word under the current cursor and enter insert mode, similar to Vim. The characters c
, i
, and w
are bound to the words change
, inside
, and word
accordingly. This satisfies a grammar expecting an ordered list of the following parts of speech: verb
, preposition
, and text-object
. Verbs and prepositions should be familiar if you’ve studied languages before. A text-object
is just a special type of a more general part of speech, a noun
, which refers to some type of text (in this case, a word
).
Consider the similar phrase:
diw
This translates to a command to delete the word under the current cursor while remaining in command mode, again similar to Vim. This uses the same grammar as before, consisting of a verb
, preposition
, and text-object
, in that order. Notice that the only letter that differs between the two inputs is the first, substituting d
for c
, where the binding d
stands for the verb delete
. Correspondingly, the only difference in the two behaviors is the final mode of the editor, namely command mode instead of insert mode. Therefore, we can surmise that the only different between the verbs delete
and change
is the mode that the editor is left in after successfully executing the command.
Not all words are used frequently enough to warrant bindings - after all, there are only so many characters on your keyboard which are convenient to type! To work around this, every word has an associated name - as we saw before, the words change
, inside
, word
, and later delete
. Commands can be formed by referring to words explicitly by name, using :
as a prefix - as in :change
. Further more, bindings are unique to each PoS - that is, the same binding can refer to different parts of speech depending on context. Consider the phrase:
ptp
This phrase uses the same verb
, preposition
, text-object
grammar as before, but introduces three new words - a verb bound to p
, a preposition bound to ‘a’, and a text-object bound to p
. The verb p
is bound to the word paste
, and is used to take the contents of the clipboard and insert it into the buffer. The text-object p
is bound to the word paragraph
; a region of contiguous text separated by newlines. Notice that the same binding, p
, is interpreted differently based on the expected part of speech!
Now, what about that new character, t
? The t
is bound to the preposition through
. The preposition
-text-object
combo (called a prepositional phrase) ip
refers to the inside of the paragraph, meaning the content between the delimiting newlines. The prepositional phrase tp
refers to the content inside of the paragraph plus the delimiter at the end - the newline. So, the full phrase ptp
translates to a command to replace the inside and final newline character of a paragraph with the contents of the clipboard. This would be useful if the text in a user’s clipboard already contains a newline in it (which would cause pip
to result in two newlines at the end of the paragraph).
Did you just read over pip
and follow along with why it would result in two newlines? I never explicitly defined what pip
does, but based on the difference between the prepositions i
and t
it makes sense - i
refers to the inside of a block of text, while t
refers to the content plus the terminal delimiter.
Pop Quiz!
What do each of the following commands do? I’ll put the answers at the bottom, but see if you can work them out based on the words described above:
piw
cip
ctp
This emphasizes the power of commands formed by composing words. So far, we’ve introduced three verbs (c
, d
, and p
for change
, delete
, and paste
), two prepositions (i
and t
for inside
and through
), and two text-objects (w
for word
and p
for paragraph
). That leads to 3 * 2 * 2 = 12 distinct (but related) commands, in exchange for learning 3 + 2 + 2 = 7 words.
What happens if we add a new word - say the text-object s
for sentence
. I bet you can guess what the sentence
text-object defines. Adding that gives us 8 total words, and 18 total commands. One new word, 6 new commands. This is the power of composition. As our vocabulary of words grows, the number of commands we can make grows combinatorially.
So how does this all work?
This post covered the model of grammars underlying Gramme, but didn’t go into details about how grammars are actually implemented in Gramme. The next post will tackle some of those implementation details, dive deeper in what exactly a “mode” is, and go over how modes relate to grammars.
Answers
piw
:paste inside word
- replace a word under the cursor with the contents of the clipboard (but leave the surrounding spaces/punctuation)cip
:change inside paragraph
- delete the paragraph under the cursor and enter insert modedtp
:delete through paragraph
- delete the paragraph under the cursor including the newline after, but stay in command mode