diff options
Diffstat (limited to 'docs/design/company-to-corfu-migration.org')
| -rw-r--r-- | docs/design/company-to-corfu-migration.org | 324 |
1 files changed, 0 insertions, 324 deletions
diff --git a/docs/design/company-to-corfu-migration.org b/docs/design/company-to-corfu-migration.org deleted file mode 100644 index 55da081c..00000000 --- a/docs/design/company-to-corfu-migration.org +++ /dev/null @@ -1,324 +0,0 @@ -#+TITLE: Design: Migrate from Company to Corfu (with prescient integration) -#+AUTHOR: Craig Jennings -#+DATE: 2026-05-15 -#+OPTIONS: toc:nil num:nil - -* Status - -Draft. - -* Problem - -The in-buffer completion stack is built on =company= (=modules/selection-framework.el:192-243=), augmented with =company-quickhelp= (doc popups), =company-box= (icon kinds), and =company-prescient= (smart sorting). The configuration works, but =company= predates the modern =completion-at-point= machinery in Emacs 29+: it maintains its own backend list (=company-backends=) parallel to =completion-at-point-functions= and routes around the built-in protocol. - -=corfu= is the modern equivalent. It drives the same UI through =completion-at-point-functions= directly, which means every Emacs mode that already publishes a capf (eglot, elisp-mode, ledger-mode, AUCTeX, etc.) lights up without a custom company backend. The plugin ecosystem (=cape=, =kind-icon=, =corfu-popupinfo=, =corfu-prescient=) covers the remaining gaps: fallback completers, icon kinds, doc popups, and prescient sorting. - -This migration replaces the =company= stack with the equivalent =corfu= stack, preserving: - -- Global in-buffer completion across prog and text modes. -- Tab to complete, =C-n=/=C-p= to navigate the candidate list. -- File-path completion (currently via =company-files=). -- Keyword completion in programming modes. -- Doc popups for the selected candidate. -- Icon kinds in the candidate list. -- prescient-based smart sorting (recency + frequency + filter). -- Disabling completion in mail compose buffers. -- Per-mode prefix length and idle delay tuning where it differs. -- Mode-specific backends (=company-ledger=, =company-auctex=, =company-shell=). - -* Goals - -1. =global-corfu-mode= replaces =global-company-mode=, with the same hook timing. -2. Every current =company-*= package and helper has a corfu-side equivalent or a documented drop. -3. Per-mode capf customizations (ledger, AUCTeX, eshell, mu4e compose) keep working. -4. prescient sorting extends from vertico (where it already runs) to corfu via =corfu-prescient=. -5. No regression in mu4e compose buffers — completion stays disabled there. - -* Non-Goals - -- Adding new completion sources beyond what =company= already provides. Source tuning is a follow-up. -- Reworking =eglot= or LSP integration. =corfu= reads =completion-at-point-functions=; eglot already publishes a capf. -- Touching =vertico=, =marginalia=, =consult=, =embark=, or =orderless=. Those operate on the minibuffer, not the in-buffer completion frontend. -- Touching the =accent= package's =accent-company= command (=modules/text-config.el:97-99=). The name shares a prefix with =company= by coincidence; it is the package's own function and does not depend on =company-mode=. - -* Current State - -** Module: =modules/selection-framework.el:192-243= - -| What | How | -|----------------------------+--------------------------------------------------------------------| -| Global activation | =:hook (after-init . global-company-mode)= | -| Keymap (active) | tab → complete, =C-n=/=C-p= → next/prev | -| Backends | =(company-capf company-files company-keywords)= | -| Idle delay | =2= seconds | -| Minimum prefix | =2= chars | -| Show numbers | =t= | -| Tooltip alignment / flip | annotations aligned, flip when above | -| Tooltip limit | =10= | -| Selection wrap | =t= | -| Require match | =nil= | -| Global disable modes | =message-mode=, =mu4e-compose-mode=, =org-msg-edit-mode= | -| Doc popups | =company-quickhelp= (=:config (company-quickhelp-mode)=) | -| Icon kinds | =company-box= (=:hook (company-mode . company-box-mode)=) | -| prescient sorting | =company-prescient= (=:config (company-prescient-mode)=) | - -** Other modules that touch company - -| Module | What | -|---------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------| -| =modules/ledger-config.el:44-47= | =company-ledger= backend added to =company-backends= after =ledger-mode= loads | -| =modules/latex-config.el:44-46= | =company-auctex= with =:init (company-auctex-init)= | -| =modules/eshell-config.el:163-171= | =company-shell= backend; eshell-mode-hook sets =company-minimum-prefix-length= and =company-idle-delay= to =2= locally, then enables =company-mode= | -| =modules/mail-config.el:319-333= | =cj/disable-company-in-mu4e-compose= calls =(company-mode -1)= in =mu4e-compose-mode-hook= and =org-msg-edit-mode-hook= | -| =modules/prog-go.el:41,50= | =(declare-function company-mode "company")= + =(company-mode)= in go-mode-hook | -| =modules/prog-python.el:28,46= | Same shape for python-mode-hook | -| =modules/prog-webdev.el:32,47= | Same shape for web-mode-hook | - -The three prog-* modules are redundant once =global-company-mode= is on; they will become redundant in the same way once =global-corfu-mode= is on. They can either be deleted outright or rewritten to ensure capfs are wired. - -* Target State - -** New configuration in =modules/selection-framework.el= - -Replace the company block with: - -#+begin_src emacs-lisp -;; ---------------------------------- Corfu ---------------------------------- -;; In-buffer completion built on completion-at-point-functions. - -(use-package corfu - :demand t - :hook (after-init . global-corfu-mode) - :bind - (:map corfu-map - ("<tab>" . corfu-complete) - ("C-n" . corfu-next) - ("C-p" . corfu-previous)) - :custom - (corfu-cycle t) ; wrap-around selection - (corfu-auto t) ; auto-popup like company - (corfu-auto-delay 2.0) ; match company-idle-delay - (corfu-auto-prefix 2) ; match company-minimum-prefix-length - (corfu-count 10) ; match company-tooltip-limit - (corfu-quit-no-match 'separator) ; quit only after explicit gap - (corfu-preview-current nil) ; no inline preview (closer to company default) - :config - ;; History so frequently-used candidates float up across sessions. - (with-eval-after-load 'savehist - (corfu-history-mode 1) - (add-to-list 'savehist-additional-variables 'corfu-history)) - ;; Mirror company-global-modes = (not message-mode mu4e-compose-mode - ;; org-msg-edit-mode): corfu has no built-in exclusion list, so the - ;; mail-config hook below toggles corfu-mode off in those buffers. - ) - -;; Doc popups for the selected candidate (company-quickhelp equivalent). -(use-package corfu-popupinfo - :ensure nil ; ships with corfu - :after corfu - :hook (corfu-mode . corfu-popupinfo-mode) - :custom - (corfu-popupinfo-delay '(0.5 . 0.2))) ; (initial . subsequent) - -;; Icon kinds (company-box equivalent). -(use-package kind-icon - :after corfu - :custom - (kind-icon-default-face 'corfu-default) - :config - (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter)) - -;; Cape: extra capfs (file paths, keywords, dabbrev, dict) so corfu -;; covers the cases company-files / company-keywords used to handle. -(use-package cape - :demand t - :config - ;; Order matters: file paths first (most specific), then keywords, - ;; then dabbrev (buffer words) as the catch-all. - (add-to-list 'completion-at-point-functions #'cape-file) - (add-to-list 'completion-at-point-functions #'cape-keyword) - (add-to-list 'completion-at-point-functions #'cape-dabbrev)) -#+end_src - -The existing =prescient= and =vertico-prescient= use-package blocks stay. =company-prescient= is replaced with =corfu-prescient=: - -#+begin_src emacs-lisp -(use-package corfu-prescient - :demand t - :after (corfu prescient) - :config - (corfu-prescient-mode)) -#+end_src - -** Setting / Package Translation Table - -| Company setting / package | Corfu equivalent | -|------------------------------+--------------------------------------------------------| -| =global-company-mode= | =global-corfu-mode= | -| =company-backends= | =completion-at-point-functions= (set by modes + cape) | -| =company-capf= | built-in (corfu reads capf directly) | -| =company-files= | =cape-file= | -| =company-keywords= | =cape-keyword= | -| =company-idle-delay= | =corfu-auto-delay= (when =corfu-auto= is =t=) | -| =company-minimum-prefix-length= | =corfu-auto-prefix= | -| =company-tooltip-limit= | =corfu-count= | -| =company-selection-wrap-around= | =corfu-cycle= | -| =company-require-match= | =corfu-quit-no-match='separator= (closest equivalent) | -| =company-show-numbers= | no direct equivalent; drop (rarely used) | -| =company-tooltip-align-annotations= | corfu does this by default | -| =company-tooltip-flip-when-above= | corfu repositions automatically | -| =company-global-modes= (excludes) | per-mode hook toggling =corfu-mode= off | -| =company-quickhelp= | =corfu-popupinfo= (ships with corfu) | -| =company-box= | =kind-icon= | -| =company-prescient= | =corfu-prescient= | -| =company-ledger= | =ledger-mode='s built-in capf (Emacs 28+) -- see below | -| =company-auctex= | AUCTeX's built-in capf + =cape-tex= -- see below | -| =company-shell= | =cape-keyword= + eshell's own pcomplete via capf | - -* Migration Steps - -Order matters: package install → core swap → per-module fixups → cleanup. - -** Step 1: install corfu-side packages - -Add to the package install list (ELPA pulls these in via use-package): - -- =corfu= -- =cape= -- =kind-icon= -- =corfu-prescient= - -(=corfu-popupinfo= ships inside =corfu= and does not need a separate install.) - -** Step 2: rewrite =modules/selection-framework.el= - -Replace lines 192-226 (the three =company-*= use-package blocks) with the corfu / cape / corfu-popupinfo / kind-icon blocks above. Replace line 240-243 (=company-prescient=) with =corfu-prescient=. Section headers update from "Company" → "Corfu". - -** Step 3: rewrite mail-compose disabling (=modules/mail-config.el:319-333=) - -Replace the =cj/disable-company-in-mu4e-compose= helper: - -#+begin_src emacs-lisp -(defun cj/disable-corfu-in-mu4e-compose () - "Disable corfu in mu4e compose buffers (and org-msg-edit-mode). -Mail composition reads more naturally without auto-popups." - (corfu-mode -1)) - -(add-hook 'mu4e-compose-mode-hook #'cj/disable-corfu-in-mu4e-compose) -(with-eval-after-load 'org-msg - (add-hook 'org-msg-edit-mode-hook #'cj/disable-corfu-in-mu4e-compose)) -#+end_src - -Also disable in =message-mode= (which company excluded via =company-global-modes=) by adding a hook: - -#+begin_src emacs-lisp -(add-hook 'message-mode-hook #'cj/disable-corfu-in-mu4e-compose) -#+end_src - -(The function name still says "mu4e-compose" but covers all three modes via the same toggle. Rename to =cj/--disable-corfu-in-mail= if that bothers; cosmetic.) - -** Step 4: rewrite =modules/ledger-config.el= - -Drop =company-ledger=. =ledger-mode= ships =ledger-complete-at-point= and registers it on =completion-at-point-functions= when the mode loads. Verify with =M-x describe-variable RET completion-at-point-functions RET= inside a ledger buffer after the migration. No new code needed unless verification shows the capf isn't being registered, in which case add a local capf push in =ledger-mode-hook=. - -** Step 5: rewrite =modules/latex-config.el= - -Drop =company-auctex= and its =(company-auctex-init)= call. AUCTeX 13+ publishes its own capf via =TeX-mode='s setup. =cape-tex= covers LaTeX macro / symbol completion as a fallback. Add to the LaTeX config: - -#+begin_src emacs-lisp -(with-eval-after-load 'tex-mode - (add-hook 'TeX-mode-hook - (lambda () - (add-to-list 'completion-at-point-functions #'cape-tex)))) -#+end_src - -** Step 6: rewrite =modules/eshell-config.el:163-171= - -Drop =company-shell= and the eshell-mode-hook =company-mode= activation. Replace with per-mode capf wiring: - -#+begin_src emacs-lisp -(add-hook 'eshell-mode-hook - (lambda () - ;; eshell publishes pcomplete-completions-at-point. cape - ;; wraps pcomplete so corfu picks it up. - (add-to-list 'completion-at-point-functions - (cape-capf-buster #'pcomplete-completions-at-point)) - (corfu-mode 1))) -#+end_src - -The =cape-capf-buster= wrapper invalidates pcomplete's cache between completion calls; without it, eshell completion staleness shows. - -** Step 7: delete the three prog-* =company-mode= calls - -In =modules/prog-go.el=, =modules/prog-python.el=, and =modules/prog-webdev.el=: - -- Remove =(declare-function company-mode "company")=. -- Remove =(company-mode)= from the mode hook (=global-corfu-mode= covers it). - -If any of the three modes needs a mode-specific capf override (most don't; eglot / language-server modes publish their own), add it in place of the deleted call. - -** Step 8: rename section header in selection-framework.el - -The header at line 189 (=;; ---- Company ----=) becomes =;; ---- Corfu ----=. Cosmetic but worth doing in the same change for grep-ability. - -** Step 9: byte-compile and uninstall company packages - -After the rewrite is green: - -- =M-x package-delete= on =company=, =company-quickhelp=, =company-box=, =company-prescient=, =company-ledger=, =company-auctex=, =company-shell=. -- Confirm =M-x list-packages= shows none of them as installed. -- Run =make clean && make compile= to refresh =.elc=. - -* Testing - -** Unit / integration - -- =tests/test-selection-framework-corfu.el= (new) - - =corfu= is required and =global-corfu-mode= is on after init. - - =completion-at-point-functions= includes =cape-file=, =cape-keyword=, =cape-dabbrev= in the global value. - - =corfu-prescient-mode= is enabled. -- =tests/test-mail-config-corfu-disable.el= (new) - - Visiting a buffer in =mu4e-compose-mode= and =message-mode= leaves =corfu-mode= disabled. -- Update =tests/test-ledger-config.el= and =tests/test-latex-config.el= (if they exist) to assert the relevant capf is registered. - -** Manual verification - -Run each: - -1. Open an =elisp= file, type =mes= → corfu popup shows =message=, =message-box=, etc. Tab completes. -2. Open a =python= file with an eglot-attached pyright, type a partial identifier → capf candidates appear via corfu. -3. Open a =.ledger= file, type a partial account → ledger's own capf surfaces matches. -4. Open a =.tex= file, type =\beg= → AUCTeX capf shows =\begin{}=, etc. -5. Open eshell, type =cd ~/=,/= → cape-pcomplete capf completes paths. -6. =C-x m= or open mu4e compose → no popup; =corfu-mode= reports as off in the mode line. -7. Recently-completed candidates float to the top after a few uses (prescient). -8. Type a partial filename in a Lisp buffer (=/etc/pas=) → =cape-file= completes =/etc/passwd= path. - -** Regression watch - -- =accent-company= (=C-`= in text modes) still opens its own popup; it doesn't depend on =company-mode=. -- =eglot= integration: capf priority should be eglot first, cape* last. If eglot completions get crowded out by =cape-dabbrev=, switch =cape-dabbrev= to a buffer-local addition only in modes that lack a richer capf. - -* Risks - -| Risk | Mitigation | -|---------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------| -| AUCTeX's built-in capf doesn't actually fire (some AUCTeX versions need a manual nudge) | Step 5 also adds =cape-tex=; verify in step-tested .tex file. | -| =cape-dabbrev= clutters language-server completions | Make =cape-dabbrev= per-mode (text modes only) if regression appears; trivial to scope down. | -| ledger-mode capf is unregistered on first buffer open | If verification fails, add =ledger-mode-hook= that pushes =ledger-complete-at-point= onto the capf list. | -| eshell pcomplete cache staleness | =cape-capf-buster= in step 6 invalidates between calls. | -| prescient sort order resets | =corfu-history-mode= + =savehist-additional-variables= preserves across sessions; prescient stays for the frequency/recency weighting. | -| Some modes (rare) only support company backends, never wrote a capf | Discovered case-by-case during step 7 verification. Worst case: keep =company= around in a tiny scope for that one mode, which defeats the migration -- unlikely. | - -* Rollback - -The change lives in one commit (or one branch). Revert restores company + the per-module integrations. =package-install= the deleted =company-*= packages back. Idempotent. - -* Effort estimate - -M (1 hour to 1 day). The rewrite of =selection-framework.el= is ~50 lines and mechanical. The per-module fixups are 5-15 lines each across six files. Testing the per-mode capfs is where the time goes. - -* Open questions - -- Keep =cape-dict= for spell-style completion in text modes? Out of scope for the migration but a natural follow-up. Decide after the base swap lands. -- Switch eshell to =eat= or =eshell-toggle= as part of this? No — out of scope. |
