Emacs After a Decade of Vim

· im tosti


It's not a secret, I've been a vim, then neovim user. Actually for longer than a decade, but it sounds better this way. This is a story about my recent (3 months) excursion into emacs.

How did I get to vim? #

I got there through the classic pipeline: nethack to vi to vim. While I've tried several other editors, I've also kept coming back to it. Generally speaking, the issues were one of a couple of flavors:

  1. Some editors I've tried, like Micro, Helix, and Lite, basically lacked functionality. While most "IDE-ish" integrations aren't of interest to me, I do like being able to (for example), see clangd warnings next to the code that's causing them. I often tend to write in languages that use s-expressions, so parinfer is of interest to me (I could learn paredit, but I'd still need paredit support anyway).
  2. Sometimes, they're just not usable. This might sound weird, so let me explain. I'm very input latency-sensitive. If I press a key, I expect the consequence of having pressed that key to show up on the screen immediately. Can it take longer? I guess, but the longer it takes, the more disruptive it is to my ability to use the thing. For example, I've tried using VS Code, Atom, and Doom Emacs (we'll get back to this one), and have had this issue with them all. When I launch a terminal emulator with a keybind, I start typing immediately, and if a single keystroke is missed, I stop using that terminal emulator / WM, unless I have no choice. Same thing applies here!
  3. Sometimes, the editor is actually quite flexible (or has what I need built-in), and it's not painfully slow. There is one last thing that can mess it up. Sometimes, the core conceptualization behind the editor is not one that I share. This is an odd thing to say, so let me give an example. Kakoune is a really interesting editor! It's fast, and has the stuff I need. However, the core things about it (shell as an executor/config language, selection-then-action) don't cleanly map to my head. It's not its fault, but I'm not motivated to change in that respect.

As I've mentioned before, vim was already familiar because of nethack, which includes the action-then-direction aspect. It was fast enough to not bother me too much, especially without any plugins installed. It had plugins for the other stuff I needed. So it's kind of been the choice by default. Every once in a while I would venture out and try something else, only to end up coming back.

How did I get to emacs? #

Emacs has long been on my list of stuff to give a serious attempt. But that list is long, and I've been bouncing around various things, and doing other stuff overall. And, you know, I didn't want to get RSI, which is certainly something you hear about a lot.

Eventually, out of a recommendation by my wife (then girlfriend, I think? it was a longtime ago), I tried Doom Emacs with evil enabled. It's basically an Emacs distribution (think lazyvim) that bundles a bunch of stuff. It includes a lot of things, takes forever to start up, and includes a vim-like leader+localleader binding scheme. And it didn't work for me at all!

It was really buggy, very VERY slow (it may have been improved since, but how should I know?), crashed on me several times, and so I just uh, didn't launch it again after a few days.

I did feel like I was missing something, so I didn't rule emacs out. I did rule out Doom Emacs though, as well as evil mode. So, a few years later, I was ready to try it again. I had some spare time in between jobs, and being able to "live in Emacs" as some people do would be a significant advantage at the newer one.

This time, besides just "using it", the goal was also to understan the Emacs way. Not just the Doom Emacs way, but how the fundamental thing under everything worked, and why it was built a particular way. So I started with literally no plugins, no configuration, and built-up from there.

To avoid burying the lede more than it already has been, I like it. I'm going to keep using it. Is it going to be my only editor? No, the same way vim has never been my only editor. But let's just say that this blog post has been written and uploaded entirely in Emacs, so that should give you a good enough idea.

Learning Emacs #

With that out of the way, please come along on my journey, where I recount the last couple of months, and how my usage of Emacs has changed over this time.

My configuration isn't quite ready to share yet, but the rate of changes is going down over time, so I should be ready to publish some things (besides hoquet) by the end of July.

Getting Situated #

Initially, I basically just used emacs to edit the emacs config. I'm pretty sure that's normal. Is that normal? I think it's normal.

There were a few immediate points of annoyance:

All of these ended up being a lot more involved than I'd have liked.

XDG #

To help out with XDG compliance I wrote a small library, appropriately loadable via (require 'xdg). This is also not quite ready yet, because I haven't solved two issues with it:

  1. What if you do want ~/.emacs to be used? (xdg/var ...) isn't appropriate in this scenario, because the way you're most likely to use it is actually targetting ~/.emacs rather than ~/.config/emacs and co. However, if I point it to user-emacs-directory, then it messes up people trying to do Other Things. I do have a solution in mind, but I'd like to ruminate on it a bit first.

  2. Would be nice to have windows support :). I mean, I don't need to have it. But emacs does support the platform. MacOS is not an issue because you can always just define the following:

    • XDG_CONFIG_HOME to ~/Library/Preferences (sacrilege, but logical)
    • XDG_CACHE_HOME to ~/Library/Caches
    • XDG_DATA_HOME and XDG_STATE_HOME to ~/Library/Application Support (there may be a better option for that one, but this isn't my use case anyway, eh?).

    With windows it's a little bit more complicated than this, so I might just never bother to be honest.

Here's the configuration for core emacs stuff (no packages) that I've had to set to stop my user dir from being polluted so far:

 1(setopt
 2 auth-sources `(,(xdg/var "authinfo")
 3                "~/.authinfo" "~/.netrc") ; compat
 4 ;; auto-save files look like this: .saves-
 5 ;; they are created when you haven't saved the file in a while,
 6 ;; and are used to restore what you were working on in case of
 7 auto-save-list-file-prefix (xdg/var! "autosave/.saves-")
 8 auto-save-file-name-transforms `((".*" ,(xdg/var "autosave/\\1") t))
 9
10 ;; when you save a file, emacs copies the old contents to a backup file,
11 ;; in case you messed something up. It can save >1 versions too.
12 backup-directory-alist `((".*" . ,(xdg/var "backup/")))
13
14 ;; the customization UI writes to this
15 ;; I don't use it, but it gets triggered by various things anyway.
16 custom-file (xdg/var "custom.el")
17
18 ;; eshell and shell modes provide a shell to slap into terminal emulators
19 eshell-directory-name          (xdg/var! "eshell")
20 eshell-history-file-name       (xdg/var "eshell/history.el")
21 eshell-last-dir-ring-file-name (xdg/var "eshell/lastdir")
22 shell-history-file-name        (xdg/var "shell/history.el")
23
24 ;; lock-files look like .#name and help when multiple users are editing
25 ;; the same files. I don't really use that functionality though.
26 lock-file-name-transforms `((".*" ,(xdg/var! "lock/\\1") t))
27
28 ;; emacs keeps track of known projects
29 project-list-file (xdg/var "projects.el")
30
31 ;; minibuffer history, especially useful for vertico
32 savehist-file (xdg/var "history.el")
33
34 ;; TRAMP is the remote editing system.
35 tramp-persistency-file-name (xdg/var "tramp/history.el")
36
37 ;; I'm honestly not sure what these are.
38 transient-levels-file  (xdg/var "transient/levels.el")
39 transient-values-file  (xdg/var "transient/values.el")
40 transient-history-file (xdg/var "transient/history.el")
41
42 ;; url-mode is the embedded browser
43 url-configuration-directory (xdg/var "url")
44 url-history-file            (xdg/var "url/history.el"))

There's even more for the packages! Though most packages I use don't need anything :)

Color Scheme #

So for this one there's good news and bad news. The good news is that emacs doesn't have the vim issue quite as much, where every package defines its own colors and does Not At All Care about what the global colors are, which is honestly a weird position to have for a predominantly terminal-based program.

The bad news is that Emacs is not a terminal-based program (I mean it CAN be, but...), so there's a lot of "faces" (basically colors, but also things like italics, background, etc).

So while a lot of things do get inheritance, the "basics" are not based on the terminal ANSI color scheme, so I'm basically back in square one. As a consequence, I took a similar approach to what I took in neovim: a base16-based generator.

So the initial version was interpolating base00 through base07, then setting base08 through base0f like I did on neovim, but then I actually read the base16 docs and followed their recommendations (based on onedark), which gave a much better result.

I stuck to that for a while, but honestly, I wasn't that comfortable requiring a package for the theme on neovim, and I'm similarly not that comfortable doing that on Emacs. I haven't done migrating off of it, but I've already got the ansi-color group defined, and then a bunch of inheritances on term-color and eat-term-color.

The plan is to later go through base16-theme-define and just set everything to be inheritance. I'd publish a package, but like I said, I do not want to need a package for this stuff. So what I'll probably do is just have a (comment-deliniated) region for setting all of the inheritances around ansi-colors, so other people making ANSI based themes can reuse the work by copy pasting it :)

Fonts #

I was like "well you know, this is a UI program, surely setting the font will be fine". And, I mean, it kind of is. Kind of.

So what's going on is that the font parser seems to vary from system to system. I haven't looked into it too deeply, but basically what I'm noticing is that variable fonts only work on some systems. So instead of a 412kb file for my variable font, I need to have 10MB of font files (on some systems only!) to actually have it work well.

The other thing to mention is that you're better off doing any UI stuff (and package.el configuration!) in early-init, simply because otherwise the window will flash a bunch while setting up if you're cold-launching.

It looks like this with my use-case:

1(add-to-list-many 'default-frame-alist
2                  '(font . "Berkeley Mono Variable 14")) ; when I can

I guess we can now talk about how I'm "growing into" the space.

Comfort #

Before we get into the elisp bits, we need to talk about RSI. I got the "Emacs pinky" within literal minutes of starting, even while just doing the above. I'll also take the opportunity to talk about the other UX packages I have installed, mostly because that's... the whole list. I don't actually have much installed besides what I'll mention here and major modes.

RSI #

So since I was interested in learning the "Emacs way", I couldn't really use Evil mode, nor Meow (which I've been told is "Evil if it was good"). My first attempt was to use God mode, but I found it really annoying how I had to "stay" in the mode. So then I found that God mode has a function that only activates it for "one" input. This was better, but still annoying for other reasons. Notably it was buggy, and I didn't really like the whole SPC thing.

Then someone showed me Devil mode, and I've never looked back. My right index finger (the comma key presser) does get a little tired, but it takes a really long time, so I'm just not that worried about it. Unfortunately, it didn't really work with Which-Key, that I do enjoy, but turns out there's a fork that mostly kind of works. I also added the ability to do ,( on top of just , and ,\n, and I was all set.

Ido #

I actually quite like Ido. The problem is, I want that kind of fuzzy selection mechanic everywhere, it just works well IMO. Initially I just set the setting for that, but that did nothing of use. So then I found some packages that helped with that, and it was kind of awkward. On top of this, I became more and more aware of just how erm, invasive Ido really was, which is a shame given that Emacs has an actual mechanism for this stuff.

By strong recommendation from my wife, I finally took a look at Vertico. I had initially disregarded vertico because the whole point for it was that it was vertical (duh), but turns out there's also Vertico flat mode, and that works fine! With that and Orderless, the main thing I felt was missing was the completion function (specifically, completion-in-region-function).

Well great news, I found a random reddit post of someone with supposedly similar sensibilities, so I did what I usually don't and copy pasted it in because, I mean, look at it, what the fuck is this, I didn't want to look into how that actually works, I just wanted completion-in-region-function to use completing-read, you know? I will look at how it works, at some later point, but realistically this is just API idiosyncracies that I don't expect to actually be interesting. :(

 1(setopt
 2 completion-in-region-function
 3 (lambda (start end collection &optional predicate)
 4   "Prompt for completion of region in the minibuffer if non-unique.
 5    Use as a value for `completion-in-region-function`.
 6    From reddit u/O10120240501, based on consult's version."
 7   (let* ((initial (buffer-substring-no-properties start end))
 8          (all (completion-all-completions initial collection predicate
 9                                           (length initial)))
10          (completion (cond
11                       ((atom all) nil)
12                       ((and (consp all) (atom (cdr all))) (car all))
13                       (t (completing-read
14                           "Completion: " collection predicate t initial)))))
15     (cond (completion (completion--replace start end completion) t)
16           (t (message "No completion") nil)))))

Yeah ok I did modify it a bit but like, I admit I have no idea what's going on here.

Misc #

Besides that I made sure to enable editorconfig, installed emacs-eat (even though I hardly use it in the end), got multiple cursors, set up parinfer (though, sadly it was through a fork, so I expect to have to revisit this at some point), and got Magit.

Magit probably deserves its own post, honestly.

But yeah I have like 15 packages installed, half of which are dependencies. I'm trying to do things the native way when it's not too painful, which has meant "more often than you'd think"!

Expand, Macro, Contract #

I'm not new to lisps, so the elisp part wasn't that bad. It's not a great lisp, but I see why attempts to make it use scheme or whatever haven't really gone anywhere: elisp contains a bunch of features specifically built for an image-based computing environment. Oh oops did I say the forbidden words? Sorry, I mean a REPL.

Examples of this include hooks, where you do not need to "define" a hook, you just kind of run-hooks. Similarly, with advices, you don't need to do dynamic bindings to do HOF shenanigans, you can just add-function or advice-add. Funnily enough, dynamic bindings actually help for this, something I've talked about before, though notably in lexical mode you still have dynamic bindings for functions and such (for this exact reason).

Initially I started defining random utilities in my init.el, but this quickly became untenable. I kind of get why people turn their init.el into an org mode literate programming file, but I can't really bring myself to do that, since I want my editor to be bootstrappable with as few steps as possible. (One of the things that bothers me the most about my neovim config, for example, is how on first launch I automatically install all the packages and compile all the tree-sitter grammars. Sometimes, I just want to do things!)

Anyway, to try and work around this I've started splitting things into pseudo-libraries. Sometimes, I could theme things, so that's what brought you (require 'xdg). Sometimes I can't, and so it goes into (require 'oven) (you can blame the Bun JS runtime for that one). We'll get to looking into what's inside of this shortly, but before we do that, we can talk about Functional Programming a bit first. Dash.el is actually pretty decent, and the whole 'it local binding thing from Common Lisp is funny for sure (and the source for the short-fn stuff in Janet and Clojure I suspect). Like I said though, I want my config to be bootstrappable, so what to do? Shrimple, I vendored it, lmao.

Yeah remember, this is free software, you can just slap a file into your elisp import location and commit it to your dotfiles. The ducks in the park are free, you can bring them home.

Anyway, now we can talk about the "dumping ground" oven.el, and also about the up-to-now unmentioned treesitter.el, which I have conflicting feelings about.

Oven #

Honestly, if you think about it, a lot of these "make sense" of sorts, but the progression is funny.

So first, I have the already shown add-to-list-many. Here's all it does:

1(defun add-to-list-many (list &rest elems)
2  "Calls add-to-list on LIST for every ELEMS."
3  (mapc (apply-partially #'add-to-list list) elems))

Why make it a macro when it can be a function, right? :)

The very next thing is a macro that fits on one line: (defmacro comment (&rest body) "Removes all child forms." '()). So the funny thing with this one is that at some point I realized Emacs does have something built-in for this, but then I quickly forgot about it again, because it's not called comment. Showing my priors, I guess. Should have made it an alias probably.

The next one is also funny, in that I no longer use it. I'll probably remove it before I publish my dotfiles for Emacs. Let's take a quick look first:

1(defun require* (feature &optional filename)
2  "Safe variant of `require' that sends a message if it fails."
3  (unless (require feature filename 'noerror)
4    (message "failed to load %s" (symbol-name feature))))

You can see what I was going for, I think. The reason I'm not actually using it is also simple. For all the uses I had for this, I now instead use this (defined in early-init.el:

1(defun load-init (name &optional noerror nomessage nosuffix must-suffix)
2  "Like `load` but relative to `xdg/etc` (my emacs-user-dir)."
3  (load (xdg/etc name) noerror nomessage nosuffix must-suffix))

Speaking of, I should probably make it just use &rest, make it shorter. You can also see the jank with xdg/etc vs ~/.emacs here.

The point is that if I require, I'm already fairly confident the feature is present, and if it doesn't, I would indeed rather it fail loudly! We'll get to the reason I can do this a little later, but these three next functions, all related, because I appreciated the related forms of these in base elisp:

1(defmacro with-hook (hook &rest body)
2  "Runs `add-hook' on HOOK, placing BODY in an implicit lambda."
3  (declare (indent defun))
4  `(add-hook ,hook (lambda () ,@body)))
5(defmacro with-abnormal-hook (hook args &rest body)
6  "Like `with-hook' but works on abnormal hooks, binding ARGS."
7  (declare (indent defun))
8  `(add-to-list ,hook (lambda ,args ,@body)))
9(defalias 'with-load #'with-eval-after-load)

We're going to skip the next three because they have to do with use-package and I want to talk about that separately, but know that when we get to the use-package story, this is where those are defined.

The final two things that I have in 'oven are actually really weird. I have an implementation of read-all and a funny function I call occur-sexpr. Let's start by looking at their implementations before I explain the why:

 1(defun read-all (&optional stream)
 2  "Read every Lisp expression as text from STREAM, return as Lisp list of
 3objects. If STREAM is nil or an unrecognized type, uses
 4`current-buffer'."
 5  (cond
 6   ((bufferp stream)
 7    (save-excursion
 8      (let ((buf (current-buffer))
 9            forms)
10        (goto-char (point-min))
11        (while (ignore-errors (push (read buf) forms)))
12        (nreverse forms))))
13   ((stringp stream)
14    (with-temp-buffer
15      (insert stream)
16      (read-all (current-buffer))))
17   (t (read-all (current-buffer)))))
18(defun occur-sexpr (sym)
19  "Show all occurrences of the function SYM being called at the top level
20of the current buffer. Intended to be used with an alias of `comment'."
21  (interactive "Search for symbol: ")
22  (let* ((buf   (get-buffer-create "*occur-sexpr*"))
23         (forms (read-all)))
24    (switch-to-buffer-other-window buf)
25    (erase-buffer)
26    (mapc (lambda (body)
27            (dolist (form (cdr body))
28              (pp form buf)))
29          (seq-filter
30           (lambda (form)
31             (eq sym (car-safe form)))
32           forms))
33    (lisp-interaction-mode)
34    (read-only-mode t)))

So first of all, there not being a read-all really confused me. I do kind of get it now, since a stream might not be terminated, but like, please let me shoot my own foot. I can handle void*, I can handle read-all. Anyway, implementing that wasn't so bad, but what's the deal with occur-sexpr? The docstring is kind of a hint.

Like I've mentioned previously, I want my init.el to be bootstrappable, not installing any packages (and thus not loading/configuring them) until I ask it to. "Ah, that's what use-package is for!" I can hear you exclaiming, but we'll get to that. Anyway, since I don't want the packages to be automatically installed, I need some way to install them. What I chose to do was to basically have instances of (comment (run me!)) where applicable (next to the configuration for the packages in init.el), so you can go over things over time. The issue, of course, is that now you gotta scroll a bunch. The reason I did it this way is because then I can have the snippet do multiple things, or maybe even a mapc and so on.

So how do I make it so you don't have to scroll? Well, initially I used occur against something like package-install, but I have several package-vc-install now too (for reasons). Ok, no problem, refine the regex. Ah, wait, it's line-oriented: literally the same problem as with the comments. Well ok, it's fine, I can just jump to each location from the occur buffer... yeah that sucks.

The solution, obviously, is to have an occur-like that operates on s-expressions... And that's what occur-sexpr is, albeit a very simplified version for my specific use-case (I told you 'oven is not made for you).

Anyway, in short, it does a read-all, and then dumps out all the s-expressions that are lists that start with sym, with sym removed and de-listified. So calling (occur-sexpr 'foo) in a buffer with the contents (foo 1 2) (foo (3)) will have 3 lines: 1, 2, and (3).

It works quite well with comment, since you can define an alias for it, making that specific unique symbol searchable. I use comment-init :)

Treesitter #

Ok I'm just going to say it. Treesitter support in Emacs is kinda bad. I've definitely seen worse, but oofie.

You basically have to modify major-mode-remap-alist, treesit-language-source-alist, manage your own install locations (if you want it to be done right), manage your own versioning, and then some.

Basically, in treesitter.el I tried to write a single place to configure all of this, notably treesit-languages-alist. I just put a bunch of stuff there and then have a variable watcher that sets the appropriate things.

Honestly, this didn't need to be a section, but I wanted to give you a break after the absolute code-dump from the previous section. I don't usually do this (either of those two things), but this is my "low(er) effort, high(er) consistency" blog, so you'll have to deal with it.

The good news is that there shouldn't be much more code around, besides some minor examples. You'll have to wait for the rest of it if you were interested, sorry!

Use-Package #

use-package is a really weird one. It's the de-facto package manager wrapper for Emacs, used by essentially everyone, and a major source of inspiration for a lot of the vim package managers. I also think it sucks. Like really badly. Just for my specific use-case, but you know.

So basically, the problem is that the way you're intended to use it is the classic "oh don't worry I'll download all your packages at startup". There's some affordances for "what if the package isn't available yet?" but it's truly an afterthought.

For example, consider use-package-check-before-init. The concept is that it will make sure your package is installed using locate-library before running its code in :init. It does do this! The problem is that things like mappings, hooks, etc: you know, the stuff you actually want out of use-package, still run unconditionally.

So ok, for a while I moved everything into :init, but like, why am I even doing this at that point?

In short, I've fully replaced my usage of use-package with the following 3 functions (just their titles and docstrings):

"Oh but the real point of use-package is that it's a macro you only need at compile time, and then your startup is fast." I hear you, yeah you, in the back. Let's talk about performance.

Performance #

So here's the deal. I don't think Emacs has bad performance, actually. I think a lot of elisp code performs really poorly, though. If you had the use-package related discussion, you might be surprised that despite being relatively eager-loaded, my emacs init time is under 0.2 seconds (around 140ms in fact). If I run emacs -nw in a terminal, I can start writing almost immediately, despite using kkp. (And yes, it does bother me that it's an "almost".) When using frames, most of the time is spent initializing the windowing system.

This is despite the fact that I have not byte-compiled any of my configuration, or even the libraries that I have so unceremoniously dumped in my elisp folder, including dash.el. And despite the fact that my early-init just flatly requires oven, dash, and whatever else.

You know what does take a long time? When I eventually press C-c c (my binding for org-capture). Suddenly, I'm sitting there waiting a solid 2+ seconds just for that to get loaded. Some packages are written more performant than others. It's that simple. Elisp is a "slow" language, but my computer is fast. No amount of fast language can save you from slow code.

Anyway, with all of this "lazy loading" stuff in use-package, surprisingly enough, before switching to use-package, despite eval-when-compile, my init time was closer to 0.4s. In short, by removing use-package, I halved my init time. I don't know why that happened, I don't know how it happened, and I don't care, because here's the deal. Emacs already has a lazy loading thing (and I'm pretty sure use-package just uses that) called autoloads. Basically, when you byte or native compile something, it will add things out of that into a big autoloads file which says "when you call this symbol, require that file". Not quite what I'm used to coming from the ksh/zsh world for that concept, but hey, it works. I'll just keep requiring my code (that I know is fast anyway), and then relying on autoload for stuff in packages (behind a with-package macro).

Coincidentally, this is also a good opportunity to talk about throughput vs latency (my favorite subject, as repeat-readers will know). Even for the really heavy configurations, once it's loaded, emacs tends to be quite snappy, despite elisp being a "slow language". Fundamentally speaking, elisp is a cons-cell based lisp with extremely dynamic (in the dynamic-binding sense, as we talked about earlier) behavior. The latter isn't great for throughput, but outside of bad code examples, tends to be fine for throughput.

Being a cons-cell based lisp though has the funny effect where data locality (and ram latency) matters a lot, both for runtime and garbage collection. Well, conveniently, with LPDDR5X and similar standards, while we're not quite back in the era of "just do linked lists, who cares": RAM is still much much slower, you can't presume that a RAM access is a CPU cycle, the actual user-performance-impact of a bunch of random memory accesses is lowered, since it's simply "fast enough". It matters for throughput (boy does it ever), but for latency, because that cost is amortized over time, it's just not that relevant. It is, but it's not huge.

Since I predominantly care about user-latency, I don't mind this stuff, within limits (I'm still upset that emacs -nw won't let me immediately type, but at least emacsclient -nw does, unless I truly abuse it).

Solving the Right Problem #

One thing I can say though is that Emacs is less efficient at editing text than vim (/neovim/helix/kakoune). The true question is, does it matter? Here's the thing: is how quickly you can edit the text really the decision-making point for you? Sure that's the core of what it does, but like, do you get the car that can do the fastest 0-100 km/h, or the one that serves all your needs? Is your actual fundamental goal to modify text, or is it to do something else?

For me, editing text is kind of a prerequisite for what I'm actually using the system for. Very rarely do I actually feel like the limiting factor for what I'm doing the speed at which I can modify the text. A not insignificant part of this is that nowadays I apparently type at up to 140 WPM (what the fuck?), so it's just really not a concern.

Sometimes, the problem is editing text efficiently, like generating a huge struct-mapping table in C. Those cases tend to be predictable though, so I can just script emacs to do it. I'll keep using neovim too, but I now have words to put on why I kept trying to move off of it, and those words are simply that it doesn't try to solve the fundamental problem I have. Modal editing does not try to solve the fundamental problem I am trying to solve. Emacs kind of does.

Conclusion #

I still have more things to say, obviously, but I think this is long enough as it is. Here's the follow-up todos:

I don't know if there's much else to do tbh.

The global conclusion is: I think I'll keep using Emacs. There's your lede :)

last updated: