From 84cbffb03939e4f462dbfdfb98a57f3cb52461d5 Mon Sep 17 00:00:00 2001 From: Szymon Szukalski Date: Tue, 7 Apr 2026 10:20:08 +1000 Subject: Add Corfu name completion setup --- .gitignore | 4 +- README.md | 15 +++- abbrev_defs | 1 + config.org | 230 +++++++++++++++++++++++++++++++++++++++++++++++++++-- name-dictionary.el | 25 ++++++ 5 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 abbrev_defs create mode 100644 name-dictionary.el diff --git a/.gitignore b/.gitignore index 6106eb5..bdf817b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ !/.gitignore !/AGENTS.md !/README.md -!/config.org \ No newline at end of file +!/config.org +!/abbrev_defs +!/name-dictionary.el diff --git a/README.md b/README.md index 922bcbe..4225d93 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,19 @@ The minibuffer stack is intentionally small: - `vertico` provides the completion UI. - `orderless` handles matching. - `marginalia` adds annotations. +- `corfu` handles in-buffer completion popups for text and Org buffers. + +Name entry uses two separate paths: + +- `abbrev` provides deterministic one-shot shortcuts for fixed name expansions. +- a small CAPF feeds Corfu a fixed list of name variants from `name-dictionary.el`. +- `M-x ss/name-dictionary-add-name` and `M-x ss/name-dictionary-remove-name` update that file and refresh the current prose buffers. +- `M-x ss/name-dictionary-add-name-from-region` uses the active region as the name being added. + +### Persistent abbrevs + +Persistent abbrevs live in `abbrev_defs` at the repository root. The config loads that file on startup, enables abbrev mode only in text-like buffers, and saves learned abbrevs back to the same file silently when buffers are saved. +The name shortcut list lives in `name-dictionary.el` so it can be managed from Emacs with a single source of truth. ### Babel tangle process @@ -89,7 +102,7 @@ Daily notes are plain Org files in `~/org/daily/`, named by date. When a daily n - `Notes` - `Open Loops` -This keeps daily capture fast without routing everything through Denote. +Daily capture stays fast without routing everything through Denote. ### Agenda usage diff --git a/abbrev_defs b/abbrev_defs new file mode 100644 index 0000000..055c8c8 --- /dev/null +++ b/abbrev_defs @@ -0,0 +1 @@ +;; Persistent abbrev definitions written by Emacs. diff --git a/config.org b/config.org index f996776..1c18407 100644 --- a/config.org +++ b/config.org @@ -181,10 +181,25 @@ This section covers global editing behavior and a few startup-time tuning choices. #+begin_src emacs-lisp + (require 'abbrev) + (set-language-environment "UTF-8") (set-default-coding-systems 'utf-8) (prefer-coding-system 'utf-8) + (setq abbrev-file-name (expand-file-name "abbrev_defs" user-emacs-directory) + save-abbrevs 'silently) + (when (file-exists-p abbrev-file-name) + (quietly-read-abbrev-file abbrev-file-name)) + + (defun ss/enable-prose-abbrev-mode () + "Enable abbrev mode in prose buffers. +We keep this mode-local so code buffers stay on their own completion rules." + (abbrev-mode 1)) + + (dolist (hook '(text-mode-hook org-mode-hook)) + (add-hook hook #'ss/enable-prose-abbrev-mode)) + (setq auto-save-default nil backup-inhibited t echo-keystrokes 0.1 @@ -226,7 +241,7 @@ folders from note helpers. * Minibuffer completion -This keeps completion close to standard Emacs behaviour while improving the +Completion stays close to standard Emacs behaviour while improving the minibuffer prompts used throughout the notes workflow. Vertico provides the UI, Orderless handles flexible matching, and Marginalia adds lightweight annotations. @@ -252,11 +267,216 @@ annotations. :after vertico :init (marginalia-mode 1)) + + (use-package corfu + :ensure t + :pin gnu + :init + ;; Enable Corfu globally so text and Org buffers get in-buffer completion + ;; popups when a CAPF provides candidates. + (global-corfu-mode 1)) +#+end_src + +* Name shortcuts + +The name workflow uses one repository file for both abbrev triggers and CAPF +candidates. Abbrev handles deterministic one-shot expansions, while Corfu-backed +CAPF completion offers explicit choice among name variants. The two mechanisms +stay separate: abbrev mutates the buffer immediately, CAPF only proposes +candidates. + +#+begin_src emacs-lisp + (defconst ss/name-dictionary-file + (expand-file-name "name-dictionary.el" user-emacs-directory) + "Persistent source of truth for name abbrevs and CAPF candidates.") + + (defvar ss/name-dictionary-entries nil + "Persistent name entries used by abbrev and CAPF.") + + (when (file-exists-p ss/name-dictionary-file) + (load ss/name-dictionary-file nil t)) + + (require 'seq) + (require 'subr-x) + + (defun ss/name-dictionary--entry-name (entry) + "Return the canonical name in ENTRY." + (plist-get entry :name)) + + (defun ss/name-dictionary--entry-abbrev (entry) + "Return the abbrev trigger in ENTRY." + (plist-get entry :abbrev)) + + (defun ss/name-dictionary--entry-aliases (entry) + "Return alias candidates in ENTRY." + (plist-get entry :aliases)) + + (defun ss/name-dictionary-default-abbrev (name) + "Suggest a short trigger for NAME." + (let* ((parts (split-string (string-trim name) "[[:space:]]+" t)) + (first (downcase (substring (car parts) 0 (min 2 (length (car parts)))))) + (last (downcase (substring (car (last parts)) 0 1)))) + (if (> (length parts) 1) + (concat first last) + first))) + + (defun ss/name-dictionary-read-aliases (prompt) + "Read PROMPT and return a cleaned alias list." + (let ((aliases (mapcar #'string-trim (split-string (read-string prompt) "," t)))) + (seq-filter (lambda (string) (not (string-empty-p string))) aliases))) + + (defun ss/name-dictionary-canonical-names () + "Return the canonical names from the dictionary." + (mapcar #'ss/name-dictionary--entry-name ss/name-dictionary-entries)) + + (defun ss/name-dictionary-candidates () + "Return all CAPF candidates from the dictionary." + (delete-dups + (apply #'append + (mapcar (lambda (entry) + (cons (ss/name-dictionary--entry-name entry) + (ss/name-dictionary--entry-aliases entry))) + ss/name-dictionary-entries)))) + + (defun ss/name-dictionary-install-abbrevs () + "Install name abbrevs into the current buffer." + (setq-local local-abbrev-table (copy-abbrev-table local-abbrev-table)) + (dolist (entry ss/name-dictionary-entries) + (when-let ((name (ss/name-dictionary--entry-name entry)) + (abbrev (ss/name-dictionary--entry-abbrev entry))) + (define-abbrev local-abbrev-table abbrev name)))) + + (defun ss/name-dictionary-refresh-buffers () + "Refresh name abbrevs in every prose buffer." + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (and (bound-and-true-p abbrev-mode) + (derived-mode-p 'text-mode 'org-mode)) + (ss/name-dictionary-install-abbrevs))))) + + (defun ss/name-dictionary-save () + "Write the name dictionary file." + (let ((print-length nil) + (print-level nil)) + (with-temp-file ss/name-dictionary-file + (insert ";; -*- lexical-binding: t; -*-\n") + (insert ";; Persistent name entries used by abbrev and CAPF.\n\n") + (insert "(setq ss/name-dictionary-entries\n '") + (insert (pp-to-string ss/name-dictionary-entries)) + (insert ")\n")))) + + (defun ss/name-dictionary-reload () + "Reload the name dictionary file and refresh prose buffers." + (interactive) + (when (file-exists-p ss/name-dictionary-file) + (load ss/name-dictionary-file nil t)) + (ss/name-dictionary-refresh-buffers) + (message "Reloaded name dictionary")) + + (defun ss/name-dictionary--upsert (entry) + "Insert or replace ENTRY in `ss/name-dictionary-entries'." + (setq ss/name-dictionary-entries + (sort + (cons entry + (seq-remove (lambda (existing) + (or (string= (ss/name-dictionary--entry-name existing) + (ss/name-dictionary--entry-name entry)) + (string= (ss/name-dictionary--entry-abbrev existing) + (ss/name-dictionary--entry-abbrev entry)))) + ss/name-dictionary-entries)) + (lambda (left right) + (string< (ss/name-dictionary--entry-name left) + (ss/name-dictionary--entry-name right)))))) + + (defun ss/name-dictionary--remove (name) + "Remove NAME from `ss/name-dictionary-entries'." + (setq ss/name-dictionary-entries + (seq-remove (lambda (entry) + (string= (ss/name-dictionary--entry-name entry) name)) + ss/name-dictionary-entries))) + + (defun ss/name-dictionary--save-entry (name abbrev aliases) + "Persist a name entry and refresh prose buffers." + (let ((entry (list :name name :abbrev abbrev))) + (when aliases + (setq entry (append entry (list :aliases aliases)))) + (ss/name-dictionary--upsert entry) + (ss/name-dictionary-save) + (ss/name-dictionary-refresh-buffers) + (message "Added name: %s" name))) + + (defun ss/name-dictionary-add-name (name abbrev aliases) + "Add a canonical NAME, ABBREV trigger, and optional ALIASES." + (interactive + (let* ((name (read-string "Full name: ")) + (abbrev (string-trim + (read-string "Abbrev trigger: " + (ss/name-dictionary-default-abbrev name)))) + (aliases (ss/name-dictionary-read-aliases + "Aliases (comma-separated, optional): "))) + (list name abbrev aliases))) + (when (string-empty-p abbrev) + (setq abbrev (ss/name-dictionary-default-abbrev name))) + (ss/name-dictionary--save-entry name abbrev aliases)) + + (defun ss/name-dictionary-add-name-from-region (beg end abbrev aliases) + "Add the active region as a name entry." + (interactive + (if (use-region-p) + (let* ((name (string-trim + (buffer-substring-no-properties + (region-beginning) (region-end)))) + (abbrev (string-trim + (read-string "Abbrev trigger: " + (ss/name-dictionary-default-abbrev name)))) + (aliases (ss/name-dictionary-read-aliases + "Aliases (comma-separated, optional): "))) + (list (region-beginning) (region-end) abbrev aliases)) + (user-error "Select a name first"))) + (let ((name (string-trim + (buffer-substring-no-properties beg end)))) + (when (string-empty-p abbrev) + (setq abbrev (ss/name-dictionary-default-abbrev name))) + (ss/name-dictionary--save-entry name abbrev aliases))) + + (defun ss/name-dictionary-remove-name (name) + "Remove NAME from the persistent dictionary." + (interactive + (list (completing-read "Remove name: " + (ss/name-dictionary-canonical-names) + nil t))) + (ss/name-dictionary--remove name) + (ss/name-dictionary-save) + (ss/name-dictionary-refresh-buffers) + (message "Removed name: %s" name)) + + (defun ss/name-dictionary-open () + "Open the persistent name dictionary." + (interactive) + (find-file ss/name-dictionary-file)) + + (defun ss/name-capf () + "Return a name completion candidate set at a word boundary." + (let ((end (point))) + (save-excursion + (skip-syntax-backward "w_") + (let ((beg (point)) + (candidates (ss/name-dictionary-candidates))) + (when (and (< beg end) candidates) + (list beg end candidates :exclusive 'no)))))) + + (defun ss/enable-name-capf () + "Add `ss/name-capf' once in prose buffers." + (unless (memq #'ss/name-capf completion-at-point-functions) + (add-hook 'completion-at-point-functions #'ss/name-capf nil t))) + + (dolist (hook '(text-mode-hook org-mode-hook)) + (add-hook hook #'ss/enable-name-capf)) #+end_src * Notes workflow -This keeps the note-taking system deliberately small. Daily notes stay as plain +The note-taking system remains deliberately small. Daily notes stay as plain Org files in =~/org/daily/=, while longer-lived notes use Denote inside the same root directory and rely on links for relationships. @@ -554,9 +774,9 @@ repo-local behavior lives with the notes tree. * Gptel workflow -This keeps LLM chat available as a small workflow tool inside Emacs. GitHub -Copilot authentication is handled on demand by gptel, so there is no token -plumbing in this file. +LLM chat remains a small workflow tool inside Emacs. GitHub Copilot +authentication is handled on demand by gptel, so there is no token plumbing in +this file. #+begin_src emacs-lisp (use-package gptel diff --git a/name-dictionary.el b/name-dictionary.el new file mode 100644 index 0000000..ccad42a --- /dev/null +++ b/name-dictionary.el @@ -0,0 +1,25 @@ +;; -*- lexical-binding: t; -*- +;; Persistent name entries used by abbrev and CAPF. + +(setq ss/name-dictionary-entries + '((:name "Ajay Shirke" :abbrev "ajs") + (:name "Akash Ali" :abbrev "aka") + (:name "Anant Sharma" :abbrev "ans") + (:name "Anish Kapoor" :abbrev "ank") + (:name "Ashish Pawar" :abbrev "asp") + (:name "Atilla Gul" :abbrev "atg") + (:name "Harjeet Singh" :abbrev "has") + (:name "Ilayaraja Selvaraju" :abbrev "ils") + (:name "Jaganmohanrao Peddada" :abbrev "jap") + (:name "Karthik Seelam" :abbrev "kas") + (:name "Kashif Hussain" :abbrev "kah") + (:name "Kenny Xu" :abbrev "kex") + (:name "Krishnaraj Muralidharan" :abbrev "krm") + (:name "Manmohan Verma" :abbrev "mav") + (:name "Mudit Sharma" :abbrev "mus") + (:name "Munesh Wali" :abbrev "muw") + (:name "Naresh Kumar Patro" :abbrev "nap") + (:name "Ramesh Sugandh Mallela" :abbrev "ram") + (:name "Shailesh Borse" :abbrev "shb") + (:name "Vinay Deo" :abbrev "vid") + (:name "Vishnu Kaarthi Thangadurai" :abbrev "vit"))) -- cgit v1.2.3