Skip to content
This repository has been archived by the owner on Mar 8, 2021. It is now read-only.

Making rxi's lite my main text editor #56

Open
a327ex opened this issue May 25, 2020 · 4 comments
Open

Making rxi's lite my main text editor #56

a327ex opened this issue May 25, 2020 · 4 comments

Comments

@a327ex
Copy link
Owner

a327ex commented May 25, 2020

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.

screenshot

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:

local draw_line_text = DocView.draw_line_text

function DocView:draw_line_text(idx, x, y)
  local spaces = get_line_indent_guide_spaces(self.doc, idx)
  local sw = self:get_font():get_width(" ")
  local w = math.ceil(1 * SCALE)
  local h = self:get_line_height()
  for i = 0, spaces - 1, config.indent_size do
    local color = style.guide or style.selection
    renderer.draw_rect(x + sw * i, y, w, h, color)
  end
  draw_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

local draw_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:

image

And changing the code a bit:

local draw_line_text = DocView.draw_line_text

function DocView:draw_line_text(idx, x, y)
  local spaces = get_line_indent_guide_spaces(self.doc, idx)
  local sw = self:get_font():get_width(" ")
  local w = sw * config.indent_size
  local h = self:get_line_height()
  for i = 0, spaces - 1, config.indent_size do
    local color
    if i % (2 * config.indent_size) == 0 then
      color = style.selection
    else
      color = style.line_number
    end
    renderer.draw_rect(x + sw * i, y, w, h, color)
  end
  draw_line_text(self, idx, x, y)
end

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:

function keymap.on_key_pressed(k)
  local mk = modkey_map[k]
  if mk then
    keymap.modkeys[mk] = true
    -- work-around for windows where `altgr` is treated as `ctrl+alt`
    if mk == "altgr" then
      keymap.modkeys["ctrl"] = false
    end
  else
    local stroke = key_to_stroke(k)
    local commands = keymap.map[stroke]
    if commands then
      for _, cmd in ipairs(commands) do
        local performed = command.perform(cmd)
        if performed then break end
      end
      return true
    end
  end
  return false
end

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:

function keymap.on_key_pressed(k)
  local mk = modkey_map[k]
  if mk then
    ...
  else
    local stroke = key_to_stroke(k)
    local commands
    if mode == "insert" then
      commands = keymap.map[stroke]
    elseif mode == "movement" then
      commands = keymap.map["modal+" .. stroke]
    end
  ...
end

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.

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:

function Doc:text_input(text)
  if self:has_selection() then
    self:delete_to()
  end
  if in_snippet then
    local moved = false
    for i, p in ipairs(snippet_insert_positions) do
      if p.snippet_number == snippet_index then
        self:insert(p.line, p.col + current_col_offset, text)
        if not moved then
          self:move_to(#text)
          moved = true
        end
      end
    end
    current_col_offset = current_col_offset + 1
  else
    local line, col = self:get_selection()
    self:insert(line, col, text)
    self:move_to(#text)
  end
end

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.

@Andre-LA
Copy link

I am very curious to test this editor, especially since I really like textadept and lite have some similarities with it.

@Skytrias
Copy link

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

@D0NM
Copy link

D0NM commented May 26, 2020

Way to go!

@a327ex
Copy link
Owner Author

a327ex commented May 28, 2020

do the snippets work nicely with indentation? had problems when imlementing my own version of snippets

They weren't but I just fixed them! Thanks for pointing this out @Skytrias

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants