<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Posts on Vadzim Shender</title><link>https://shender.org/posts/</link><description>Recent content in Posts on Vadzim Shender</description><generator>Hugo</generator><language>en</language><lastBuildDate>Thu, 16 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://shender.org/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Making backspace in vertico behave like ido</title><link>https://shender.org/vertico-backspace-like-ido/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://shender.org/vertico-backspace-like-ido/</guid><category>Emacs</category><description>After many years of ido muscle memory, the one thing that kept biting me after switching to vertico was backspace: ido&amp;rsquo;s file history drops you into a directory, so backspace goes up one directory at a time right away, while vertico&amp;rsquo;s recalls the full file path — and backspace first has to delete the file name character by character. A two-dozen-line replacement for vertico-directory-delete-char restores the ido feel — and improves on it.</description><content:encoded>&lt;p&gt;My Emacs file-finding habits go back a long way. I lived with vanilla &lt;code&gt;find-file&lt;/code&gt; until I discovered ido-mode, and that was a big step up in UX. Later I moved most completion to &lt;a href="https://github.com/emacs-helm/helm"&gt;helm&lt;/a&gt; — but &lt;code&gt;C-x C-f&lt;/code&gt; stayed on ido the whole time: when &lt;code&gt;helm-mode&lt;/code&gt; takes over the standard completion commands, &lt;code&gt;helm-completing-read-handlers-alist&lt;/code&gt; lets you add per-command exceptions, and mine said &lt;code&gt;(find-file . ido)&lt;/code&gt;. Last December I finally rewrote my config around the modern stack — &lt;a href="https://github.com/minad/vertico"&gt;vertico&lt;/a&gt;, &lt;a href="https://github.com/oantolin/orderless"&gt;orderless&lt;/a&gt;, &lt;a href="https://github.com/minad/consult"&gt;consult&lt;/a&gt; — and the &lt;a href="https://github.com/minad/vertico/wiki#make-vertico-and-vertico-directory-behave-more-like-ivyido"&gt;vertico wiki&lt;/a&gt; even has a recipe for making it behave more like ido. Most of my muscle memory survived the move.&lt;/p&gt;
&lt;p&gt;Except backspace.&lt;/p&gt;
&lt;p&gt;Here is the difference. In &lt;code&gt;ido-find-file&lt;/code&gt;, &lt;code&gt;M-p&lt;/code&gt;/&lt;code&gt;M-n&lt;/code&gt; move you between the &lt;em&gt;directories&lt;/em&gt; you previously opened files in: you land in a directory, and from there each backspace takes you one level up. It is navigation, not text editing. Vertico&amp;rsquo;s &lt;code&gt;M-p&lt;/code&gt; instead recalls the full path of a previously opened &lt;em&gt;file&lt;/em&gt;, inserted into the minibuffer as ordinary editable text — which I actually like more: the file I want is often one I already opened. And when it is not, a recalled path is still my starting point. In a project I know well, &lt;code&gt;project-find-file&lt;/code&gt; is only half of how I move around; just as often I navigate relative to a file I touched recently — a habit formed years before project.el or projectile existed: recall it, delete its name, go a level or two up or into a sibling directory. Which means pressing backspace — and this is where it breaks. Ordinary text gets ordinary editing: backspace deletes one character, except right after a slash, where &lt;code&gt;vertico-directory-delete-char&lt;/code&gt; removes a whole path component. So I hold backspace to erase the long file name; it eats the name character by character — and the moment it crosses the slash, the still-held key starts deleting whole directories. Release a beat too late and half the path is gone.&lt;/p&gt;
&lt;p&gt;The fix is a small replacement for &lt;code&gt;vertico-directory-delete-char&lt;/code&gt;: when the point is at the end of the minibuffer and the input as a whole is an existing file path, treat backspace as &amp;ldquo;delete the last component&amp;rdquo; — exactly what my fingers expect after ido. This assumes the &lt;code&gt;vertico-directory&lt;/code&gt; extension (shipped with vertico) is loaded, as in the wiki recipe above.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-elisp" data-lang="elisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/-vertico-directory-delete-char&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Delete N directories or characters before point.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Like &lt;/span&gt;&lt;span class="ss"&gt;`vertico-directory-delete-char&amp;#39;&lt;/span&gt;&lt;span class="s"&gt;, but when point is at the end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;of the minibuffer and the input is an existing file path, delete the
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;entire path component instead of a single character.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;interactive&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;p&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cond&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; Active region: let `delete-backward-char&amp;#39; handle it.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;use-region-p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;delete-active-region&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-no-warnings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;delete-backward-char&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; At a directory boundary, try to go up a directory. Also at end of&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; input, if the whole path is an existing local file, treat DEL as&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; directory-up (delete the last path component). Skip remote paths to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; avoid blocking on network round-trips.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;char-before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="sc"&gt;?/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eobp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;mb&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;minibuffer-contents-no-properties&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;file-remote-p&lt;/span&gt; &lt;span class="nv"&gt;mb&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file-exists-p&lt;/span&gt; &lt;span class="nv"&gt;mb&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;vertico-directory-up&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; Otherwise, delete a character.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;t&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-no-warnings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;delete-backward-char&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the binding, replacing the stock one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-elisp" data-lang="elisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;keymap-set&lt;/span&gt; &lt;span class="nv"&gt;vertico-map&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;DEL&amp;#34;&lt;/span&gt; &lt;span class="nf"&gt;#&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;my/-vertico-directory-delete-char&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;file-exists-p&lt;/code&gt; check is the heart of it, not a safety net. It is what distinguishes a path you just recalled from history (it exists — backspace removes the whole name) from a name you are typing right now (it does not exist yet — backspace fixes the typo, character by character). Orderless filter input like &lt;code&gt;init compl&lt;/code&gt; is likewise unaffected: such input rarely names an existing file. Two more details: remote paths are skipped on purpose, because &lt;code&gt;file-exists-p&lt;/code&gt; on a TRAMP path is a network round-trip, and backspace is the last key that should ever block on the network; and mid-path behavior is stock — after a slash, backspace deletes a whole path component; inside a name, a single character.&lt;/p&gt;
&lt;p&gt;One honest limitation: while erasing a typed name character by character, you can land on an existing prefix (deleting &lt;code&gt;Makefile2&lt;/code&gt; and reaching &lt;code&gt;Makefile&lt;/code&gt;), and the next backspace deletes a whole component. In practice, I have yet to be bitten by it.&lt;/p&gt;
&lt;p&gt;The result: recall a path from history, press backspace once — the file name is gone; every further press climbs one directory, deliberately. No more holding the key down, no more runaway deletion, and years of finger habits work again.&lt;/p&gt;
&lt;p&gt;History recall is where the stock behavior hurt the most, but the same rule pays off in ordinary navigation too: while the name you are typing matches no existing file, backspace edits it character by character; the moment the input is a complete file name — typed in full or completed with TAB — a single backspace removes it whole. Funnily enough, ido is worse here: whenever there is text in the minibuffer, it deletes character by character. My ido habits, it turns out, were really about directory-oriented history — and where the two behaviors differ, I like the new one more.&lt;/p&gt;
&lt;p&gt;To be clear, none of this is a critique of the stock &lt;code&gt;vertico-directory&lt;/code&gt; behavior — my replacement is simply what fits my habits better, and I hope it may be useful if your years of ido left you with the same directory-oriented reflexes.&lt;/p&gt;
&lt;p&gt;Here is &lt;a href="https://github.com/vshender/emacs-config/commit/0516e63138c3abcc3b1e30c3352bebde34597d23"&gt;the commit&lt;/a&gt; in my config, with the &lt;code&gt;use-package&lt;/code&gt; wiring around it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (2026-07-20):&lt;/strong&gt; Daniel Mendler, the author of vertico, &lt;a href="https://github.com/vshender/emacs-config/commit/0516e63138c3abcc3b1e30c3352bebde34597d23#commitcomment-192882975"&gt;responded&lt;/a&gt;: the stock behavior is deliberate — chosen precisely because of the &lt;code&gt;Makefile&lt;/code&gt;-prefix case above. My replacement is just a compromise that works better for me — as noted above, that case has yet to bite me in practice. He also suggested &lt;a href="https://github.com/karthink/consult-dir"&gt;consult-dir&lt;/a&gt;, and it is a nice alternative for the same need: quickly pick a recent directory and, if needed, continue navigating from there — exactly the workflow I was trying to streamline. I am keeping my replacement — it works well with my habits — but I will be using consult-dir alongside it: a great alternative way to do the same thing, and quite possibly a more efficient one.&lt;/p&gt;</content:encoded></item></channel></rss>