You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Mar 8, 2021. It is now read-only.
A few weeks ago rxi's lite text editor caught my attention. I've known of rxi for a while now because he wrote some libraries for Lua and LÖVE that I ended up using and they're generally very minimalistic and useful. He has a really good feel for uncomplicating things, or for really picking the right boundaries between concepts and ideas in a way that brings a lot of clarity.
So when I saw this editor and that it was written mostly in Lua - especially after seeing how its plugins were written - I started thinking about making it my main editor. My main editor is vim, which makes this task seem fairly complicated on first analysis. Vim has very specific ways of behaving that are really annoying to emulate to completion, but I figured that if I could implement a subset of it in this editor I'd be fairly well set, since I don't use all the functionality that vim has. So that's what I set out to do!
vim + easy motion
The first functionality to implement was vim's modality. When pressing escape you should go into normal mode, which enables you to use keys such as hjkl to move around. Looking around for another plugin that did something similar yielded no results, but I noticed that the way plugins were written was fairly interesting:
localdraw_line_text=DocView.draw_line_textfunctionDocView:draw_line_text(idx, x, y)
localspaces=get_line_indent_guide_spaces(self.doc, idx)
localsw=self:get_font():get_width("")
localw=math.ceil(1*SCALE)
localh=self:get_line_height()
fori=0, spaces-1, config.indent_sizedolocalcolor=style.guideorstyle.selectionrenderer.draw_rect(x+sw*i, y, w, h, color)
enddraw_line_text(self, idx, x, y)
end
This piece of code is from the indentguide plugin, which implements indent guides. If you're not familiar with Lua, the first line
localdraw_line_text=DocView.draw_line_text
is storing the draw_line_text function from the DocView class into the draw_line_text local variable. Then, the DocView.draw_line_text function is defined below again, but this time with an additional amount of code that defines the behavior for this plugin, and right below that the original function is called. I don't know if there's a name for this but it's a common pattern in Lua, and it's also the same as what happens with inheritance generally.
Because the editor is written mostly in Lua, this means that every function can be hooked into like this, and looking at other plugins this happens in lots of them, so it seems to be the way to go. In the case of the vim plugin I wanted to implement, it means that to add modality, I'd have to hook into whatever function handles key input and then add my own code such that when a key like h is pressed, instead of inserting h as text, it moves the cursor back one character, which is what happens in vim.
But before we move on to doing that, when I was looking at this function I also noticed this line:
renderer.draw_rect(x+sw*i, y, w, h, color)
When I read this I realized that rxi respects me as a human being. The fact that I have direct access to the most basic functions that make up the editor, like renderer.draw_rect or renderer.draw_text, without them being hidden away behind 5 million layers of abstraction, or being entirely unacessible, means that rxi is saying: "I trust that you are a grown up programmer and that you can do it, I believe in you", and for that, I say thank you. Thank you for trusting me, rxi. :)
One of the most infuriating things about LOTS of software is that this kind of trust doesn't exist. For some entirely mysterious reason, most software is written such that this level of clarity and access just isn't present. The web in general is perhaps the most egregious example of this, the fact that I can't just tell the computer to draw a rectangle somewhere should strike people as ridiculous, but for some reason most people are just fine with it. I know that there's probably a really good reason for this to be the case with the web in particular, but I really wish more software was written like rxi decided to do for this editor. I could also go on about how the entirety of this editor really captures the philosophy of the Lua language itself, but I'll leave that for another time.
In any case, one of the benefits of his decision here is that, for instance, I can just change the way the indent guides are drawn to be just how I want them. Here's what they look like with the code I posted above:
This is a fairly simple change, instead of drawing a thin like that has a width of 1, we draw a line that has the width of config.indent_size. And then we alternate the colors of each line depending on its position. And the result is that now the indent guides look exactly like how I have them on vim:
Now, going back to vim's modality, I said I needed to hook into the editor's key input function and add the behavior I wanted there. After looking around the codebase for a while I found the function responsible for this:
functionkeymap.on_key_pressed(k)
localmk=modkey_map[k]
ifmkthenkeymap.modkeys[mk] =true-- work-around for windows where `altgr` is treated as `ctrl+alt`ifmk=="altgr" thenkeymap.modkeys["ctrl"] =falseendelselocalstroke=key_to_stroke(k)
localcommands=keymap.map[stroke]
ifcommandsthenfor_, cmdinipairs(commands) dolocalperformed=command.perform(cmd)
ifperformedthenbreakendendreturntrueendendreturnfalseend
To summarize, this function takes the inputted key, checks to see if it corresponds to any command in the keymap.map table, and then executes that command. What I want to do is for it to execute another command if we're normal mode, and so the change I need to make is this:
Here, instead of taking the key pressed directly to the keymap.map table, we prefix it with "modal+", which will represent all commands happening in normal/movement/modal mode. Here's what part of that table filled with those commands looks like:
And here's what adding commands looks like:
And then it's essentially a matter of time as you add all the commands you care about and handle the transitions between insert/movement mode.
I also ended up implementing my own version of vim-easymotion, which had its own difficulties but it works along the same lines as adding vim's modality. For easymotion to work you have to also hook into the key input function and then handle different key presses at different stages of the process. Easymotion also has an additional complexity which involves changing the color of various characters, and this basically has to be done by redrawing the document's lines in very specific ways, but since the editor gives you freedom to draw text at the character level this isn't a problem that requires any hacks, just normal text manipulation work. This is what the easymotion functionality looks like:
With easymotion and the most basic vim functions I had pretty much all I needed. Sadly, I decided to not implement one of the most important features of vim which is its ability to combine commands. So, for instance, while I could make it so that pressing d in normal mode deletes a line, I can't have it such that dw deletes a word, because I didn't implement these kinds of combinations. They're not necessarily difficult, since the mechanism is the same that drives what makes easymotion work, but the amount of raw work required would be too high and I just couldn't bother. While I'm used to deleting words by instinctively pressing dw, I can rearrange most of these instincts around other keys. I mapped the deletion of a word to q instead, for instance.
After finishing this plugin (which took me about 1 week of on/off work) I was pretty confident that I had a good understanding of the editor and how to write plugins for it. So I set out to write a snippets plugin. I use this fairly often with vim so it's a good next step. This is how it turned out:
This setup is sort of inspired by the way UltiSnips works, but I think most snippet plugins work this way. Each insertion point is defined by $number and then pressing tab moves to the next number until the end. The most trouble was implementing input when you have multiple numbers that are the same, like $1 in the gif above. But again, because you have fairly fine control over everything in the editor, implementing this is a matter of putting in the work. There are no hacks required, you don't have to go deep into tens of layers of abstraction. In this specific case, whenever there's text input and you're in a specific snippet insert position you just copy the input to all positions of the same number:
One of the interesting things about both this function and the key press functions in the vim/easymotion example is that I'm not using the technique of saving the previous function in another variable and then adding to it. Sometimes you can't really do that because you have to insert code in the middle of the function and/or change its conditional flow. This poses a problem, because if another plugin also rewrites this function then it will destroy the function I defined in my own plugin. I don't know exactly how this should be handled, if plugins should take special care to see if other plugins are installed and behave differently or not, but it can lead to a problem.
One TERRIBLE solution to this problem that I see in other software is just not exposing the function in question, in this case the Doc.text_input function, and simply allowing you to hook into callbacks that are placed in specific positions in the function. So you get things like pre_whatever or post_whatever callbacks which are absolutely wrong, in my opinion. It's much better for you to be able to do what's happening here, and whatever problems that occur will just occur and then plugin writers will figure it out amongst themselves, than to litter the code these kinds of callback indirections.
Other plugins
Finally, I implemented 2 other plugins: autoindent and the gruvbox theme. autoindent is a modification of lfautoinsert, which automatically adds block finishers (like end) to your code as well as automatically indenting the cursor whenever you go to a new line. I never really liked automatic insertion of text (other than snippets) in any editors I used so my changes were to remove that aspect of it and keep only the indentation. Additionally, I also added some code to make this plugin work better with my vim plugin, because I often use o in normal mode to add a new line, and if I hadn't added specific code to handle that case it wouldn't automatically indent the new line.
The other plugin was a port of the gruvbox theme. This is fairly simple since it's already something fully supported by the editor and it's just a matter of creating a simple file:
Conclusions
All the code I wrote can be found here and if you're the kind of person who likes customizing your editor then I'd highly recommend trying lite out, especially if you like writing Lua code.
I'd also say that the editor is written in such a clear that manner that even if rxi were to suddenly disappear, I'd feel pretty comfortable with updating the editor myself for the future and fixing whatever I'd need fixed, which gives me a lot of peace of mind with making the change to making this my main editor. I certainly can't hack on vim or VsCode or Atom as easily because even though the source code to those editors might be open, they're huge projects full of complexities that would take me a lot of time to internalize. It took me a total of 1 week to internalize most of lite to the point where I'm comfortable with it even if the author stops updating it, which is a really good sign.
The text was updated successfully, but these errors were encountered:
i really want kakoune like movement, maybe i'll try that after using your vim movement! nice write up, do the snippets work nicely with indentation? had problems when imlementing my own version of snippets
A few weeks ago rxi's lite text editor caught my attention. I've known of rxi for a while now because he wrote some libraries for Lua and LÖVE that I ended up using and they're generally very minimalistic and useful. He has a really good feel for uncomplicating things, or for really picking the right boundaries between concepts and ideas in a way that brings a lot of clarity.
So when I saw this editor and that it was written mostly in Lua - especially after seeing how its plugins were written - I started thinking about making it my main editor. My main editor is vim, which makes this task seem fairly complicated on first analysis. Vim has very specific ways of behaving that are really annoying to emulate to completion, but I figured that if I could implement a subset of it in this editor I'd be fairly well set, since I don't use all the functionality that vim has. So that's what I set out to do!
vim + easy motion
The first functionality to implement was vim's modality. When pressing
escape
you should go into normal mode, which enables you to use keys such ashjkl
to move around. Looking around for another plugin that did something similar yielded no results, but I noticed that the way plugins were written was fairly interesting:This piece of code is from the indentguide plugin, which implements indent guides. If you're not familiar with Lua, the first line
is storing the
draw_line_text
function from theDocView
class into thedraw_line_text
local variable. Then, theDocView.draw_line_text
function is defined below again, but this time with an additional amount of code that defines the behavior for this plugin, and right below that the original function is called. I don't know if there's a name for this but it's a common pattern in Lua, and it's also the same as what happens with inheritance generally.Because the editor is written mostly in Lua, this means that every function can be hooked into like this, and looking at other plugins this happens in lots of them, so it seems to be the way to go. In the case of the vim plugin I wanted to implement, it means that to add modality, I'd have to hook into whatever function handles key input and then add my own code such that when a key like
h
is pressed, instead of insertingh
as text, it moves the cursor back one character, which is what happens in vim.But before we move on to doing that, when I was looking at this function I also noticed this line:
When I read this I realized that rxi respects me as a human being. The fact that I have direct access to the most basic functions that make up the editor, like
renderer.draw_rect
orrenderer.draw_text
, without them being hidden away behind 5 million layers of abstraction, or being entirely unacessible, means that rxi is saying: "I trust that you are a grown up programmer and that you can do it, I believe in you", and for that, I say thank you. Thank you for trusting me, rxi. :)One of the most infuriating things about LOTS of software is that this kind of trust doesn't exist. For some entirely mysterious reason, most software is written such that this level of clarity and access just isn't present. The web in general is perhaps the most egregious example of this, the fact that I can't just tell the computer to draw a rectangle somewhere should strike people as ridiculous, but for some reason most people are just fine with it. I know that there's probably a really good reason for this to be the case with the web in particular, but I really wish more software was written like rxi decided to do for this editor. I could also go on about how the entirety of this editor really captures the philosophy of the Lua language itself, but I'll leave that for another time.
In any case, one of the benefits of his decision here is that, for instance, I can just change the way the indent guides are drawn to be just how I want them. Here's what they look like with the code I posted above:
And changing the code a bit:
This is a fairly simple change, instead of drawing a thin like that has a width of
1
, we draw a line that has the width ofconfig.indent_size
. And then we alternate the colors of each line depending on its position. And the result is that now the indent guides look exactly like how I have them on vim:Now, going back to vim's modality, I said I needed to hook into the editor's key input function and add the behavior I wanted there. After looking around the codebase for a while I found the function responsible for this:
To summarize, this function takes the inputted key, checks to see if it corresponds to any command in the
keymap.map
table, and then executes that command. What I want to do is for it to execute another command if we're normal mode, and so the change I need to make is this:Here, instead of taking the key pressed directly to the
keymap.map
table, we prefix it with "modal+", which will represent all commands happening in normal/movement/modal mode. Here's what part of that table filled with those commands looks like:And here's what adding commands looks like:
And then it's essentially a matter of time as you add all the commands you care about and handle the transitions between insert/movement mode.
I also ended up implementing my own version of vim-easymotion, which had its own difficulties but it works along the same lines as adding vim's modality. For easymotion to work you have to also hook into the key input function and then handle different key presses at different stages of the process. Easymotion also has an additional complexity which involves changing the color of various characters, and this basically has to be done by redrawing the document's lines in very specific ways, but since the editor gives you freedom to draw text at the character level this isn't a problem that requires any hacks, just normal text manipulation work. This is what the easymotion functionality looks like:
With easymotion and the most basic vim functions I had pretty much all I needed. Sadly, I decided to not implement one of the most important features of vim which is its ability to combine commands. So, for instance, while I could make it so that pressing
d
in normal mode deletes a line, I can't have it such thatdw
deletes a word, because I didn't implement these kinds of combinations. They're not necessarily difficult, since the mechanism is the same that drives what makes easymotion work, but the amount of raw work required would be too high and I just couldn't bother. While I'm used to deleting words by instinctively pressingdw
, I can rearrange most of these instincts around other keys. I mapped the deletion of a word toq
instead, for instance.If anyone wants to implement a vim emulator for lite the code I wrote for this plugin could be a good starting point: https://github.com/a327ex/lite-plugins/blob/master/plugins/modalediting.lua
Snippets
After finishing this plugin (which took me about 1 week of on/off work) I was pretty confident that I had a good understanding of the editor and how to write plugins for it. So I set out to write a snippets plugin. I use this fairly often with vim so it's a good next step. This is how it turned out:
This setup is sort of inspired by the way UltiSnips works, but I think most snippet plugins work this way. Each insertion point is defined by
$number
and then pressing tab moves to the next number until the end. The most trouble was implementing input when you have multiple numbers that are the same, like$1
in the gif above. But again, because you have fairly fine control over everything in the editor, implementing this is a matter of putting in the work. There are no hacks required, you don't have to go deep into tens of layers of abstraction. In this specific case, whenever there's text input and you're in a specific snippet insert position you just copy the input to all positions of the same number:One of the interesting things about both this function and the key press functions in the vim/easymotion example is that I'm not using the technique of saving the previous function in another variable and then adding to it. Sometimes you can't really do that because you have to insert code in the middle of the function and/or change its conditional flow. This poses a problem, because if another plugin also rewrites this function then it will destroy the function I defined in my own plugin. I don't know exactly how this should be handled, if plugins should take special care to see if other plugins are installed and behave differently or not, but it can lead to a problem.
One TERRIBLE solution to this problem that I see in other software is just not exposing the function in question, in this case the
Doc.text_input
function, and simply allowing you to hook into callbacks that are placed in specific positions in the function. So you get things likepre_whatever
orpost_whatever
callbacks which are absolutely wrong, in my opinion. It's much better for you to be able to do what's happening here, and whatever problems that occur will just occur and then plugin writers will figure it out amongst themselves, than to litter the code these kinds of callback indirections.Other plugins
Finally, I implemented 2 other plugins: autoindent and the gruvbox theme. autoindent is a modification of lfautoinsert, which automatically adds block finishers (like
end
) to your code as well as automatically indenting the cursor whenever you go to a new line. I never really liked automatic insertion of text (other than snippets) in any editors I used so my changes were to remove that aspect of it and keep only the indentation. Additionally, I also added some code to make this plugin work better with my vim plugin, because I often useo
in normal mode to add a new line, and if I hadn't added specific code to handle that case it wouldn't automatically indent the new line.The other plugin was a port of the gruvbox theme. This is fairly simple since it's already something fully supported by the editor and it's just a matter of creating a simple file:
Conclusions
All the code I wrote can be found here and if you're the kind of person who likes customizing your editor then I'd highly recommend trying lite out, especially if you like writing Lua code.
I'd also say that the editor is written in such a clear that manner that even if rxi were to suddenly disappear, I'd feel pretty comfortable with updating the editor myself for the future and fixing whatever I'd need fixed, which gives me a lot of peace of mind with making the change to making this my main editor. I certainly can't hack on vim or VsCode or Atom as easily because even though the source code to those editors might be open, they're huge projects full of complexities that would take me a lot of time to internalize. It took me a total of 1 week to internalize most of lite to the point where I'm comfortable with it even if the author stops updating it, which is a really good sign.
The text was updated successfully, but these errors were encountered: