aboutsummaryrefslogtreecommitdiff
path: root/docs/design/company-to-corfu-migration.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design/company-to-corfu-migration.org')
-rw-r--r--docs/design/company-to-corfu-migration.org324
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.