My last post was a tour of Emacs31, which has since moved into pretest. Sean Whitton announced31.0.91,the second pretest, on July 23rd, and that's what I'm writing this on.One of the sections in that post was a short list of new completionknobs, three lines of config I mentioned and moved on from. Those threelines turned into a month-long experiment, so they get their own post.I know how that title reads, so let me put it in context before youclose the tab. It comes from someone who ran company, moved to Corfu,and then left Corfu behind for icomplete, which has been my weapon ofchoice for years now, for both the minibuffer and in-buffer completion.And I didn't just use icomplete. I sent patches to it.bug#75784,which brought vertical in-buffer behavior and prefix indicators toicomplete, is mine, and I wrote about the road to ithere andhere. So when I say I turned icompleteoff for a month, understand that I was arguing with my own past self,and that the title isn't me saying the last few years were wasted. It'sme saying the floor moved.What made me try was Emacs 31 itself. The *Completions* bufferimproved nicely while I wasn't looking, and I wanted to know whether theUI I'd been layering on top of it was still earning its place.While I was drafting this, Protesilaos published a video on thesame territory. I haven'twatched it all the way through yet, and I'm publishing anyway, becausewhat follows is my month with this setup rather than a summary of his.His videos have shaped a lot of how I think about Emacs, so go watch ittoo. I'll probably end up fixing some of my own setup after I do.Everything below runs on stock Emacs 31. Save the snippets into atest.el and launch with:emacs -Q --load test.elThe full playground file (search for test.el) is at the bottom ofthe post if you'd rather skip ahead.Two completions, one bufferEmacs completes in two places, and it's worth keeping them apart inyour head:✔️ Minibuffer completion. M-x, C-x C-f, C-x b, everycompleting-read prompt in every package you have. You type in theminibuffer, candidates come from a completion table.✔️ In-buffer completion. completion-at-point, the thing bound toM-TAB, and to TAB if you set tab-always-indent to complete.You type in a normal buffer and the candidates come fromcompletion-at-point-functions: elisp symbols, file names, whateveryour LSP server offers through Eglot.Corfu and company handle the second case. Vertico, ivy and friendshandle the first. You probably run one of each. Emacs covers both withthe same *Completions* buffer, which surprised me, and it means oneblock of config does the whole job.The base configThis is what I run:(defun my/minibuffer-truncate-lines () "Keep minibuffer lines unwrapped." (setq truncate-lines t))(use-package minibuffer :ensure nil :bind ( :map minibuffer-visible-completions-up-down-map ("C-n" . minibuffer-next-completion) ("C-p" . minibuffer-previous-completion)) :hook ((minibuffer-setup . cursor-intangible-mode) (minibuffer-setup . my/minibuffer-truncate-lines)) :custom (tab-always-indent 'complete) (completion-auto-help t) (completion-auto-select t) (completion-eager-update t) (completion-eager-display t) (minibuffer-visible-completions 'up-down) (completion-ignore-case t) (completion-show-help nil) (completion-styles '(partial-completion flex initials)) (completions-format 'one-column) (completions-max-height 10) (completions-sort 'historical) (enable-recursive-minibuffers t) (read-buffer-completion-ignore-case t) (read-file-name-completion-ignore-case t) (minibuffer-prompt-properties '(read-only t intangible t cursor-intangible t face minibuffer-prompt)) (minibuffer-depth-indicate-mode t) (minibuffer-electric-default-mode t))Two of those carry the post, and both are new in Emacs 31::custom;; ...(completion-eager-update t)(completion-eager-display t);; ...completion-eager-display decides whether *Completions* shows up onits own. completion-eager-update decides whether it refreshes as youtype. Both default to 'auto in Emacs 31, which means "only if thecompletion table asks for it," and almost nothing asks. Set both to tand you stop summoning the buffer with TAB. It's already open.Run emacs -Q, hit M-x, type wind, and you get nothing but your owntext in the minibuffer. Load the config above, do the same, and 34candidates appear the moment you stop typing. Keep typing move-del andthe list narrows to 5 without you touching a key that isn't a letter.completions-format 'one-column and completions-max-height 10 arewhat turn that list into a vertical one. The default is 'horizontal,which packs candidates across the window like ls output. One column,capped at ten lines, reads as a dropdown.completions-sort 'historical sorts alphabetically and then floatswhatever you picked recently to the top. It's the smallest of thesesettings and the one I'd miss first.That exact moment, below: four letters typed into M-x, no TABpressed, 34 candidates already sitting there in one column.completion-auto-select t moves point into the *Completions* windowwhen TAB pops it up, so you can navigate the list without a secondkey to get there. completion-show-help nil drops the two-line"Click or type RET on a completion to select it" banner from the top ofthe buffer, which I've read enough times for one lifetime.enable-recursive-minibuffers plus minibuffer-depth-indicate-modebelong together: the first lets you start a minibuffer command frominside another one, the second puts a [2] in the prompt so you knowhow deep you are. minibuffer-electric-default-mode hides the(default foo) part of a prompt as soon as you type, and brings itback if you erase.The minibuffer-prompt-properties line keeps point out of theread-only prompt text, and cursor-intangible-mode onminibuffer-setup-hook is what enforces it. truncate-lines in theminibuffer keeps long candidates on one line instead of ballooning theminibuffer to N rows.One of those settings needs a keybinding to go with it. Withminibuffer-visible-completions set to 'up-down, the arrow keys movepoint inside *Completions* while you're still typing in theminibuffer, and RET takes the highlighted candidate instead of yourliteral input. Left and right still move in the minibuffer, which iswhy I use 'up-down rather than t.By the way, I tend to avoid arrow keys, M-doesn't feel natural in a completion list for me. That's why I setC-n and C-p like this::bind (:map minibuffer-visible-completions-up-down-map ("C-n" . minibuffer-next-completion) ("C-p" . minibuffer-previous-completion))Below, the same list after a couple of C-n. The highlight has walkeddown to windmove-delete-default-keybindings while the cursor is stillin the minibuffer, where I'm still typing.The same buffer, in your codeThis setting is the one that made the experiment worth running::custom;; ...(tab-always-indent 'complete);; ...TAB in a code buffer now indents the line if it needs indenting, thencompletes if it doesn't. Open *scratch*, type (window-lay, hitTAB. Emacs fills in the common prefix, window-layout-. Hit TABagain and *Completions* opens with the five commands that match, withpoint already inside it thanks to completion-auto-select. C-n tothe one you want, RET, done.The same keys and config as the minibuffer half, in a code buffer now.After the second TAB, below: the buffer holds (window-layout-, thefive commands sit underneath, and the first is already selected.*Completions* is a real window, so it takes room in your frame insteadof floating over it. After a month I've stopped noticing, and I gotsomething back for it: the candidate list is an ordinary buffer, so Ican search it, scroll it, and yank out of it.Where LSP completion goes wrongOne place did annoy me enough to write code.My completion styles are::custom;; ...(completion-styles '(partial-completion flex initials));; ...flex is the fuzzy one: it matches an in-order subset of what youtyped, so faL finds faLinkedin and faList without you spellingout the middle. Exactly what I want from an LSP buffer, where Ihalf-remember a name and want to see the family.But flex does two jobs, and only one of them is filtering. Itstry-completion also merges the surviving candidates and inserts theresult. With tab-always-indent set to complete, that merge happenson TAB, before you ever see the list.The case that cost me an afternoon comes from this blog's own source.components/footer.tsx imports a handful of FontAwesome icons. I wantedfaLinkedin, so I typed faL and pressed TAB once:;; I typed: faL;; TAB left me with: faEnvelopeSquareAnd no list is shown, faEnvelopeSquare matches faL under flexbecause the l sits inside "Envelope", and that's where the mergelanded. TAB typed sixteen characters I didn't ask for, hid thecandidates I did want, and left me deleting.The frame below shows the whole thing. The echo area tells me Complete, but not unique, which is Emacs admitting it guessed and still showingme none of the alternatives:That's the gap I kept hitting between the default UI and what icompletehad trained me to expect. icomplete shows me candidates on TAB. Hereit types at me.Eglot ran into this too and works around it with its owneglot--dumb-flex style, which skips the merge. That fixes the typingand loses the scoring, so candidates come back in whatever order theserver sent them.I wanted both: flex's filtering and relevance sorting, and noinsertion until I pick something. So I wrapped it.flex-noinsert(defun my/flex-noinsert-try-completion (string table pred point) "Flex `try-completion' that never auto-extends the input on TAB.The stock `flex' completion style does two jobs: it filterscandidates by fuzzy (subsequence) match, and its `try-completion'merges the surviving candidates, inserting their common expansioninto the buffer. With `tab-always-indent' set to `complete' thatmerge means TAB silently types a candidate (often a far, wrong one)*before* the *Completions* list is shown. Eglot's own`eglot--dumb-flex' avoids the merge but gives no relevance sorting.This wrapper keeps flex's filtering and scoring (so prefix matchessort first, fuzzy ones last) but suppresses the merge: - no candidates -> nil (no match) - exactly one candidate -> complete it fully (TAB still finishes a unique completion) - two or more candidates -> return STRING unchanged, so TAB only pops the *Completions* list and lets you pick, inserting nothing.STRING, TABLE, PRED and POINT are the usual `try-completion' args." (let ((all (completion-flex-all-completions string table pred point)))(cond ((null all) nil) ((= (safe-length all) 1) (let ((sole (car all)))(if (string= sole string) t (cons sole (length sole))))) (t (cons string point)))));; Register the `flex-noinsert' style: same filtering/sorting as;; `flex', but with the wrapper above as its try function.(add-to-list 'completion-styles-alist '(flex-noinsert my/flex-noinsert-try-completion completion-flex-all-completions "Flex matching that never extends input on TAB."));; Reuse flex's metadata tweak so *Completions* sorts by flex score.(put 'flex-noinsert 'completion--adjust-metadata 'completion--flex-adjust-metadata)A completion style in Emacs is four things in a list: a name, atry-completion function, an all-completions function, and a docstring. I reuse completion-flex-all-completions unchanged, sofiltering and scoring are stock flex, and I swap in atry-completion that refuses to merge. The completion--adjust-metadataproperty is what keeps *Completions* sorted by flex score rather thanalphabetically.Then aim it at Eglot only::custom;; ...(completion-category-overrides '((eglot-capf (styles flex-noinsert))));; ...Minibuffer completion keeps stock flex, where the merge behavior isfine because you can see what it did. Eglot's eglot-capf categorygets the no-insert variant.Now in the same situation as before, trying to complete faL withTAB. The buffer keeps saying faL, and *Completions* opensinstead.It opens with 2664 candidates, which sounds absurd until you look atwhat's on top: faL itself, then faLeaf, faLess, faLine. Flexmatches loosely and scores tightly, so I get a huge tail under a headthat's already correct. The trade only works if nothing gets typed intomy buffer first.One more letter sharpens it. faLi cuts the list to 859, and thevisible window holds the family I was after:;; faLi + TAB;;;; *Completions*;; faLine @fortawesome/free-brands-svg-icons;; faLink @fortawesome/free-solid-svg-icons;; faList @fortawesome/free-solid-svg-icons;; faLinux @fortawesome/free-brands-svg-iconsEach one appears twice, once for the package and once for the deepimport path, and Eglot shows me which module it would pull from. C-nand RET, or keep typing. Unique completions still finish on the firstTAB, because that's the one case where the merge can't be wrong.Here is faL with the fix in place. Emacs wrote nothing into thebuffer, and the long tail sits under a head that starts where I wasaiming:And faLi, where the whole visible window is the answer:Living with itA month in and:✔️ The minibuffer half is a straight swap. Take the drawing awayand the functionality is exactly what I already had withicomplete-vertical-mode. M-x, C-x b, C-x C-f: samefiltering, same narrowing as I type, same keys under the samefingers. My hands and eyes resynced in a day or two, which is thepart nobody can argue you into. I'm not tolerating this until I goback to icomplete. It's an alternative with a different UI and thesame behavior, and I'm running it. The list being a real buffer is abonus on top.✔️ The in-buffer half surprised me. I assumed I'd miss a floatingpopup within a week. It took a couple of days to stop thinking aboutit, and splits are what sold me. Complete inside the left window ofa C-x 3 and *Completions* opens in that window, under the codeI'm completing. The right split never moves. The list arrives whereI'm already looking, and the rest of the frame stays where I leftit.A two-window frame below, avatar.tsx on the left and footer.tsx onthe right. I completed on the left, and that's the only half thatchanged:✔️ flex-noinsert is doing real work. Without it I'd have given upon the default and gone back inside a week.✔️ The scoring is where the defaults still trail. A dedicated fuzzymatcher does better when I type three letters from the middle of aname. flex gets close without getting there.IMPORTANT: I haven't deleted icomplete from my config, and I'm nottelling you to delete yours. And if you enjoy Vertico, Corfu, companyor anything else in that family, please keep using it. Those areexcellent projects, with real care put into how they look and feel,and which one you run is a personal preference, not a correctnesscontest. Nothing here is an argument against them.I wanted to show how good, and how complete, the default completion hasbecome. That's all. It's really cool now, and it's sitting in the editoryou already have.What changed for me is that I now know what the default does. When I goback to icomplete I'll be choosing it rather than inheriting it.If you run Emacs Solo, youdon't have to take my word for any of it, because the switch is alreadywired up:(setq emacs-solo-enable-icomplete nil) ; plain *Completions*(setq emacs-solo-enable-icomplete t) ; icomplete, the defaultIt's a defcustom, so M-x customize-variable RET emacs-solo-enable-icomplete works too. Flip it, restart, and live withthe other side for a week. Emacs Solo adjusts the related settings foryou: with icomplete off it sets completion-eager-display to t ratherthan 'auto, which is the setting that makes *Completions* show up onits own.Give each side long enough that you stop noticing it. After one dayyou'll only know which one feels unfamiliar, and that's a differentquestion from which one you want.The full test.elSave this, run emacs -Q --load test.el, and poke at it. Emacs 31 ornewer, since completion-eager-update, completion-eager-display andminibuffer-visible-completions-up-down-map are all new here.;;; test.el --- default completions playground -*- lexical-binding: t -*-(defun my/minibuffer-truncate-lines () "Keep minibuffer lines unwrapped." (setq truncate-lines t))(defun my/flex-noinsert-try-completion (string table pred point) "Flex `try-completion' that never auto-extends the input on TAB.Keeps flex's filtering and scoring but suppresses the merge: - no candidates -> nil (no match) - exactly one candidate -> complete it fully - two or more candidates -> return STRING unchanged, so TAB only pops the *Completions* list.STRING, TABLE, PRED and POINT are the usual `try-completion' args." (let ((all (completion-flex-all-completions string table pred point)))(cond ((null all) nil) ((= (safe-length all) 1) (let ((sole (car all)))(if (string= sole string) t (cons sole (length sole))))) (t (cons string point)))))(use-package minibuffer :ensure nil :bind ( :map minibuffer-visible-completions-up-down-map ("C-n" . minibuffer-next-completion) ("C-p" . minibuffer-previous-completion)) :hook ((minibuffer-setup . cursor-intangible-mode) (minibuffer-setup . my/minibuffer-truncate-lines)) :custom (tab-always-indent 'complete) (completion-auto-help t) (completion-auto-select t) (completion-eager-update t) (completion-eager-display t) (minibuffer-visible-completions 'up-down) (completion-ignore-case t) (completion-show-help nil) (completion-styles '(partial-completion flex initials)) (completion-category-overrides '((eglot-capf (styles flex-noinsert)))) (completions-format 'one-column) (completions-max-height 10) (completions-sort 'historical) (enable-recursive-minibuffers t) (read-buffer-completion-ignore-case t) (read-file-name-completion-ignore-case t) (minibuffer-prompt-properties '(read-only t intangible t cursor-intangible t face minibuffer-prompt)) (minibuffer-depth-indicate-mode t) (minibuffer-electric-default-mode t) :config (add-to-list 'completion-styles-alist '(flex-noinsert my/flex-noinsert-try-completion completion-flex-all-completions "Flex matching that never extends input on TAB.")) (put 'flex-noinsert 'completion--adjust-metadata 'completion--flex-adjust-metadata));;; test.el ends hereThings to try once it's loaded:✔️ M-x and type a few letters, then C-n and C-p through the list.✔️ C-x C-f and walk a directory tree with the same keys.✔️ In *scratch*, type (window-lay and hit TAB twice.✔️ C-x C-f into a project with an LSP server, M-x eglot, and type apartial method name followed by TAB to watch flex-noinsert behave.And C-x C-f doing the same job on a directory, below:All of it lives in Emacs Solotoo, my no-external-packages config, if you'd rather read it incontext.Now, go watch Prot's take after this one.Happy hacking!