summaryrefslogtreecommitdiff
path: root/config.org
diff options
context:
space:
mode:
authorSzymon Szukalski <szymon@szymonszukalski.com>2026-04-07 10:20:08 +1000
committerSzymon Szukalski <szymon@szymonszukalski.com>2026-04-07 10:20:08 +1000
commit84cbffb03939e4f462dbfdfb98a57f3cb52461d5 (patch)
tree033264241a10e21c26fc8d6c0ea1a0565aeac73b /config.org
parent8a492e8e5b55bd6c61fd55482f7d20b2a147b3ff (diff)
Add Corfu name completion setup
Diffstat (limited to 'config.org')
-rw-r--r--config.org230
1 files changed, 225 insertions, 5 deletions
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