blog > Jeanine: a 2d text editor proof of concept (09 Aug 2023)
https://github.com/yugecin/jeanine
It's about time I make this project public and write something about it, just for the sake of having the idea and this implementation out in the open, so maybe others can be inspired.
Check out related work
I think this idea struck me at 2019/2020, probably while looking at the graph representation of disassembled code in IDA (The Interactive Disassembler).
One of the main thoughts I had was that I wanted to shove functions to the side. People always say you have to split up code and functions should be short, but then you have a lot of functions that may only get used only once cluttering your file. So it seemed like a nice idea to be able to move those functions to the side and only have "entry" functions in the main view of a source file.
Another possible benefit I thought was to better visualize code flow, depending on how much the code is split into different blocks. Though this may be more applicable for assembly code like shown in IDA than typical high-level language code.
Some history:
(most of following bullet points can be expanded for images and/or more info)
(this is a list of <detail>
elements, your browser should support clicking
it to expand/collapse)
Move: h j k l ^ $ w b e gg G ^D ^U { } Insert: i I a A o O p P Delete: x dw db dd dj dk diw di' di\" di[ di( di{ da' da\" da[ da( da{ d$ Change: cw cb ciw ci' ci\" ci[ ci( ci{ ca' ca\" ca[ ca( ca{ c$ r s Indent: << >> (< > in selection) Other: . u J Select: ctrl-v Copy: yy (y in selection) View: z Search: / n N *
One thing I have configured in my vim is that the caret's color depends on the corrent mode; red while in normal mode and green while in insert mode. So of course I had to have that here too.
This is by no means an exhaustive implementation of vim keybindings, but it has the essentials (meaning the things I use the most, though not even all of that).
Another thing that's also special I think is that moving the cursor while having the primary button held down will just move the caret, there's no mouse selection mode. Initially because I hadn't implemented mouse selection at the start, but I kind of like this. As a vim user, I don't really use mouse selection anyways, and this is a nice way of moving the caret with the mouse (since you can click and move to make sure your caret is placed where you need it, even if you're not precise enough on the first click).
Splitting into blocks can be done by engaging visual line mode (ctrl-v),
optionally expanding it (by going down/up with j/k, o
to jump between start/end), and then typing the command :spl
.
Splitting will create a block with the selected lines, linking the block to the previous block (if applicable) with a bottom link. It will also create a new block for the rest (bottom part) of the content that is now split off from the initial block (if applicable), linking that block with a bottom link to the new block.
Changing links can be done with the :link <bot|right|top> <id>
command. The id
part is first number you see in the headers of each panel.
Panels get ids assigned automatically (but you could change them by modifying the
Jeanine comments, see How block configuration is stored).
(I knew beforehand which ids each block would have in this demo, so that's why I could
put in those command without really looking)
While normal (non-secondary) links define a clear parent-child relationship and
influences the positioning of the child based on the parent's position, secondary
links just add another line from block to block to mark related code but serve
no other functionality. They can be added with
:slink <bot|right|top> <id>
and removed by doing
:unlink <bot|right|top> <id>
.
Secondary links are shown in a slightly lighter gray color.
Blocks can be deleting by deleting all lines that define it (any linked panels will be re-linked to the root node). Or raw mode can be used to delete Jeanine comments (see How block configuration is stored).
Block are positioned manually.
Yes there is a check that will deny linking if it would cause a cyclic dependency.
Zooming is done by scrolling the mousewheel while holding control.
Clicking in any block while zoomed will reset the zoom smoothly, while keeping the exact position where the mouse clicked in the same spot (placing the caret doesn't seem so precise in this demo though somehow). No key input is accepted while zoomed.
Panning can be done by dragging the background, or by scrolling (holding shift while scrolling will scroll horizontally).
Raw mode can be toggled by the :raw
command. Raw mode shows the
contents without parsing Jeanine comments and shows all content in one block.
The exact caret position will be kept while toggling.
:prefs
will open the preferences editor. It shows some instructions,
some line-commands and the preferences contents itself. The line-commands are just
lines that can be double-clicked or pressed ENTER on and they will do
things. Pressing ENTER while in the preferences content block will
apply its contents. Pressing ESC will save and exit the preferences
editor. There is a line-comment to exit without saving, too.
Preferences are stored to a file defined by the JEANINE_PREFERENCES_FILE
environment variable. If this variable is not present, a warning will be shown first
before the preferences editor is shown.
:font
will open the font picker. it will show panels that are prefilled
with a list of fonts, list of font sizes, list of flags. They can be double-clicked
or pressed ENTER on and the thing the line describes will be applied.
Saving the font configuration can be done by opening the preferences editor and using the line-command to apply the current font settings.
Jeanine calculates everything based on the width/height of the 'm' character. That means non-monospace fonts may make thing look funky, like the caret showing on a wrong position.
Very naive meaning: block comment state does not go beyond block boundaries, and it is not checked if comment tokes are inside strings or not.
Search mode can be engaged by pressing /. Afterwards, n and N can be used to find the next/previous occurrence.
* can be used to quick search the symbol under the caret. This search doesn't search for isolated words matching the word under the caret, unlike vim, it's just a simple substring search.
Trailing whitespace will be highlighted in a customizable color, except if it is on the current line and the current mode is insert mode.
It is very bare bones and doesn't have things like warnings when closing while modifications haven't been saved yet, but it is in a usable state for me. It's been a long time since I've done decent work on this project, and I don't expect to do so very soon. I want to, but there's a lot of other things I want to do, too.
Just a few things form the top of my head. I don't plan to make this an editor with a lot of functionality, it works in what it's supposed to do and I find it works well for me for my use cases so far (which is basically only C so far).
The configuration of the blocks/panels is stored in the source file, as C-style block comments.
Have a look at these source files (and their git history) to get a good idea of what the impact is: (and maybe download Jeanine and open the files in it!)
readme-jeanine-comments.txt
:
Jeanine comments ================ Jeanine comments specify how to layout the source text. They start with a jeanine: prefix, followed by the directive character and a colon (like p:), followed by properties (like i:1;), optionally followed by a colon and another directive with properties and so on. There may only be maximum one jeanine comment on a single line. Currently, both directives and property names are a single character in length. Property values can be of any length, but they cannot include a semicolon. Currently C-style block comments are always used. If a value is a floating point number, it must include a decimal point. Examples: /*jeanine:p:i:1;p:0;a:b;x:0;y:30;*/ jeanine: standard jeanine comment prefix p: "panel" directive i:1;p:0;a:b;x:0;y:30; properties and their values /*jeanine:s:a:b;i:2;:s:a:b;i:3;*/ jeanine: standard jeanine comment prefix s: "secondary link" directive a:b;i:2; properties and their values : directive separator s: "secondary link" directive a:b;i:3; properties and their values | Panel directive (p) | ------------------- | These define a "panel", which is a section of text. Everything from the start | of a panel directive until the next panel directive (or EOF) will be put in | the same panel. Panel directives should be placed on a dedicated source line. | The start of the source implicitely defines the root panel, which has an id | of 0. The root panel is the only panel that has no parent. | | | | Properties | | ---------- | | - a: the anchor which specifies how this panel is attached to its parent: | | - b: bottom (bottom of parent linked to top of child) | | - t: top (top of parent linked to top of child) | | - r: right (requires a right link location directive; see below) | | - i: the id of this panel | | - p: the id of the parent of this panel | | - x: the x-offset where this panel is located, relatively to the standard | | location as determined by the anchor. If this is a float value, | | it is a multiple of the font width, otherwise it's in pixels. | | - y: the y-offset where this panel is located, relatively to the standard | | location as determined by the anchor. If this is a float value, | | it is a multiple of the font height, otherwise it's in pixels. | Right link directive (r) | ------------------------ | These define the location where panels with a 'right' anchor are linked. | Since a right link is linked at a specific line, it needs an additional | jeanine directive to know at which line it is linked (unlike top and bottom | links, which are always at panel boundaries). | | Using a property in the panel directory to store the line number where the | link is located would be less suitable, since it will possibly be incorrect | after making edits in the source while not in jeanine/2d mode. Putting a | jeanine comment at the end of the line that is linked, will survive those | edits. | | | | Properties | | ---------- | | - i: the id of the child panel that is linked from here | Secondary link directive (s) | ---------------------------- | While panels are already linked by means of properties in the panel directive, | secondary links can also be made so it is possible to have multiple links to | the same panel. The difference between primary and secondary links is that | a primary link defines the location of the child. A panel must always have a | primary link (except for the root panel). Secondary links are outgoing, | meaning they are placed at the parent's position and link to a child. | | Secondary links with a top or bottom anchor can be placed in a jeanine comment | anywhere within the panel's region, but are usually put at the end. They must | be in a jeanine comment that has its own dedicated line. That comment may then | not contain any directives that don't require a dedicated line (like a | secondary link with a right anchor). | | Secondary links with a right anchor must be placed at the end of the line | where the link should be. | | | | Properties | | ---------- | | - i: the id of the panel that is linked here | | - a: anchor: | | - b: bottom (bottom of parent linked to top of child) | | - t: top (top of parent linked to top of child) | | - r: right (this line at the parent to top of child) | Legacy right link directive (l) | ------------------------------- | These were in use when jeanine comments weren't fully specced out yet. They | are deprecated and won't newly appear any more, but might still be interpreted | for backward compatibility reasons. The 'l' initially stood for 'link'. This | directive is the functional equivalent of 1..n right link directives (r). | | This directive doesn't have properties, but rather a comma separated list of | ids that denote the child ids. (This inconsistency is the reason that it is | deprecated) | | | | Syntax | | ------ | | /*jeanine:l:2,1*/ | | jeanine: standard jeanine comment prefix | | l: "link" directive | | 2,1 child panel ids that are right-linked here (2 and 1)
So far I've only really used this editor on two C projects; basdon and nfsu2-re.