diff options
| -rw-r--r-- | modules/flyspell-and-abbrev.el | 250 |
1 files changed, 141 insertions, 109 deletions
diff --git a/modules/flyspell-and-abbrev.el b/modules/flyspell-and-abbrev.el index 978f8208..12e0d348 100644 --- a/modules/flyspell-and-abbrev.el +++ b/modules/flyspell-and-abbrev.el @@ -4,65 +4,76 @@ ;;; Commentary: ;; WORKFLOW: -;; C-' is now my main interface for all spell checking. +;; This module provides intelligent spell checking with automatic abbreviation +;; creation to prevent repeated misspellings. ;; -;; The workflow is that it finds the nearest misspelled word above where the -;; cursor is, allows for saving or correcting, then stops. You may proceed to -;; the next misspelling by selecting C-' again. +;; KEYBINDINGS: +;; C-' - Main spell check interface (cj/flyspell-then-abbrev) +;; C-c f - Toggle flyspell on/off (cj/flyspell-toggle) +;; M-o - Access 'other options' during correction (save to dictionary, etc.) ;; -;; Use M-o to get to 'other options', like saving to your personal dictionary. +;; SPELL CHECKING WORKFLOW: +;; 1. Press C-' to start spell checking +;; 2. Finds the nearest misspelled word above the cursor +;; 3. Prompts for correction or allows saving to personal dictionary +;; 4. Press C-' again to move to the next misspelling +;; 5. Each correction automatically creates an abbrev for future auto-expansion ;; -;; Flyspell will automatically run in a mode appropriate for the buffer type -;; - if it's a programming mode, it will only check comments -;; - if in text mode, it will check everything -;; - otherwise it will turn off. -;; This check happens on every mode switch. +;; FLYSPELL ACTIVATION: +;; Flyspell is NOT automatically enabled. You activate it manually: +;; - C-c f - Toggle flyspell on (uses smart mode detection) or off +;; - C-' - Runs flyspell-buffer then starts correction workflow ;; -;; If you want flyspell on in another mode (say fundamental mode), or you want -;; to turn it off, you can toggle flyspell's state with 'C-c f' +;; When enabled, flyspell adapts to the buffer type: +;; - Programming modes (prog-mode): Only checks comments and strings +;; - Text modes (text-mode): Checks all text +;; - Other modes: Must enable manually with C-c f ;; -;; The nicest thing is that each spell correction creates an abbrev. This -;; essentially is a shortcut that expands that same misspelling to the correct -;; spelling the next time it's typed. That idea comes courtesy Artur Malabarba, -;; and it's increased my overall typing speed. +;; ABBREVIATION AUTO-EXPANSION: +;; Each spell correction creates an abbrev that auto-expands the misspelling +;; to the correct spelling when you type it in the future. This significantly +;; increases typing speed over time. ;; -;; Original idea here: +;; Original idea from Artur Malabarba: ;; http://endlessparentheses.com/ispell-and-abbrev-the-perfect-auto-correct.html ;; -;; The code below is my refactoring of Artur Malabarba's code, and using -;; flyspell rather than ispell. -;; ;; NOTES: -;; -;; FYI, the keybinding typically taken for the flyspell-mode-map "C-;" has -;; been deliberately hijacked in custom-functions.el for my personal-keymap. -;; This is the code run there: - -;; (eval-after-load "flyspell" -;; '(define-key flyspell-mode-map (kbd "C-;") nil)) +;; The default flyspell keybinding "C-;" is unbound in this config as it's +;; used for the custom keymap (cj/custom-keymap). ;;; Code: +;; Forward declarations +(eval-when-compile (defvar org-dir)) +(defvar flyspell-mode) +(defvar ispell-list-command) +(defvar ispell-skip-region-alist) +(declare-function flyspell-overlay-p "flyspell") +(declare-function flyspell-auto-correct-previous-hook "flyspell") +(declare-function flyspell-correct-at-point "flyspell-correct") +(declare-function flyspell-buffer "flyspell") +(declare-function flyspell-prog-mode "flyspell") +(declare-function thing-at-point "thingatpt") + ;; ----------------------------------- Abbrev ---------------------------------- -(use-package abbrev-mode - :ensure nil - :defer 0.5 - :custom - (abbrev-file-name (concat user-emacs-directory "assets/abbrev_defs")) - :config - (abbrev-mode 1)) +;; Defer abbrev configuration until first use +(with-eval-after-load 'abbrev + (setq abbrev-file-name (concat user-emacs-directory "assets/abbrev_defs")) + (setq save-abbrevs 'silently)) + +;; Enable abbrev-mode by default (lightweight, okay to do eagerly) +(setq-default abbrev-mode t) ;; ---------------------------- Ispell And Flyspell ---------------------------- (use-package ispell - :defer .5 :ensure nil ;; built-in + :commands (ispell-word ispell-region ispell-buffer) :config - ;; (setopt ispell-alternate-dictionary - ;; (concat user-emacs-directory "assets/english-words.txt")) - (setopt text-mode-ispell-word-completion nil) - (setopt ispell-alternate-dictionary nil) + ;; Use setq consistently (setopt requires Emacs 29+) + (setq text-mode-ispell-word-completion nil) + (setq ispell-alternate-dictionary nil) (setq ispell-dictionary "american") ; better for aspell ;; use aspell rather than ispell @@ -76,7 +87,7 @@ (setq ispell-extra-args '("--sug-mode=ultra" "-W" "3" "--lang=en_US")) (setq ispell-local-dictionary "en_US") (setq ispell-local-dictionary-alist - '(("en_US" "[[:alpha:]]" "[^[:alpha:]]" "['‘’]" + '(("en_US" "[[:alpha:]]" "[^[:alpha:]]" "[''']" t ;; Many other characters ("-d" "en_US") nil utf-8))) ;; personal directory goes with sync'd files @@ -86,8 +97,8 @@ (add-to-list 'ispell-skip-region-alist '("^#+BEGIN_SRC" . "^#+END_SRC"))) (use-package flyspell - :after (ispell abbrev) :ensure nil ;; built-in + :commands (flyspell-mode flyspell-prog-mode flyspell-buffer) :config ;; unset keybinding as we're using it for cj/custom keymap (keymap-unset flyspell-mode-map "C-;") @@ -95,73 +106,77 @@ (setq flyspell-issue-message-flag nil)) (use-package flyspell-correct - :after flyspell - :defer .5) + :commands flyspell-correct-at-point) ;; ------------------------------ Flyspell Toggle ------------------------------ ;; easy toggling flyspell and also leverage the 'for-buffer-type' functionality. -;; (defun flyspell-toggle () -;; "Turn Flyspell on if it is off, or off if it is on. - -;; When turning on,it uses `flyspell-on-for-buffer-type' so code-vs-text is -;; handled appropriately." -;; (interactive) -;; (if (symbol-value flyspell-mode) -;; (progn ; flyspell is on, turn it off -;; (message "Flyspell off") -;; (flyspell-mode -1)) -;; ;; else - flyspell is off, turn it on -;; (progn -;; (flyspell-on-for-buffer-type) -;; (message "Flyspell on")))) -;; (define-key global-map (kbd "C-c f") 'flyspell-toggle ) +;;;###autoload +(defun cj/flyspell-toggle () + "Turn Flyspell on if it is off, or off if it is on. + +When turning on, it uses `cj/flyspell-on-for-buffer-type' so code-vs-text is +handled appropriately." + (interactive) + ;; Check if spell checker is available + (unless (or (executable-find "aspell") + (executable-find "ispell") + (executable-find "hunspell")) + (user-error "No spell checker found. Install aspell, ispell, or hunspell")) + + (if (bound-and-true-p flyspell-mode) + (progn ; flyspell is on, turn it off + (flyspell-mode -1) + (message "Flyspell off")) + ;; else - flyspell is off, turn it on + (cj/flyspell-on-for-buffer-type) + (message "Flyspell on"))) ;; ------------------------ Flyspell On For Buffer Type ------------------------ ;; check strings and comments in prog mode; check everything in text mode -;; (defun flyspell-on-for-buffer-type () -;; "Enable Flyspell for the major mode and check the current buffer. - -;; If flyspell is already enabled, do nothing. If the mode is derived from -;; `prog-mode', enable `flyspell-prog-mode' so only strings and comments get -;; checked. If the buffer is text based `flyspell-mode' is enabled to check -;; all text." -;; (interactive) -;; (unless flyspell-mode ; if not already on -;; (cond -;; ((derived-mode-p 'prog-mode) -;; (flyspell-prog-mode) -;; (flyspell-buffer) -;; ((derived-mode-p 'text-mode) -;; (flyspell-mode 1) -;; (flyspell-buffer)))))) - -;; (add-hook 'after-change-major-mode-hook 'flyspell-on-for-buffer-type) -;; (add-hook 'find-file-hook 'flyspell-on-for-buffer-type) +(defun cj/flyspell-on-for-buffer-type () + "Enable Flyspell for the major mode and check the current buffer. + +If flyspell is already enabled, do nothing. If the mode is derived from +`prog-mode', enable `flyspell-prog-mode' so only strings and comments get +checked. If the buffer is text based `flyspell-mode' is enabled to check +all text." + (interactive) + (unless (bound-and-true-p flyspell-mode) ; if not already on + (cond + ((derived-mode-p 'prog-mode) + (flyspell-prog-mode) + (flyspell-buffer)) + ((derived-mode-p 'text-mode) + (flyspell-mode 1) + (flyspell-buffer))))) + +;; Note: NOT auto-enabling on hooks - user activates manually with C-' or C-c f +;; (add-hook 'after-change-major-mode-hook 'cj/flyspell-on-for-buffer-type) +;; (add-hook 'find-file-hook 'cj/flyspell-on-for-buffer-type) ;; ---------------------------- Flyspell Then Abbrev --------------------------- ;; Spell check the buffer and create abbrevs to avoid future misspellings. -(setq-default abbrev-mode t) - (defun cj/find-previous-flyspell-overlay (position) "Locate the Flyspell overlay immediately previous to a given POSITION." - ;; sort the overlays into position order - (let ((overlay-list (sort (overlays-in (point-min) position) + (require 'cl-lib) + ;; Sort overlays into position order (non-destructive copy) + (let ((overlay-list (sort (cl-copy-list (overlays-in (point-min) position)) (lambda (a b) (> (overlay-start a) (overlay-start b)))))) - ;; search for previous flyspell overlay + ;; Search for previous flyspell overlay (while (and overlay-list (or (not (flyspell-overlay-p (car overlay-list))) - ;; check if its face has changed + ;; Check if its face has changed (not (eq (get-char-property (overlay-start (car overlay-list)) 'face) 'flyspell-incorrect)))) (setq overlay-list (cdr overlay-list))) - ;; if no previous overlay exists, return nil + ;; If no previous overlay exists, return nil (when overlay-list - ;; otherwise, return the overlay start position + ;; Otherwise, return the overlay start position (overlay-start (car overlay-list))))) @@ -172,6 +187,7 @@ beginning of the misspelled word. Setting the hook on pre-command ensures that any started Flyspell corrections complete before running other commands in the buffer." (interactive "d") + (require 'thingatpt) ;; Lazy-load only when function is called (add-hook 'pre-command-hook (function flyspell-auto-correct-previous-hook) t t) (let* ((overlay-position (cj/find-previous-flyspell-overlay position)) @@ -182,32 +198,48 @@ buffer." (downcase misspelled-word) nil))) +;;;###autoload (defun cj/flyspell-then-abbrev (p) - "Call \='flyspell-correct-at-point\=' and create abbrev for future corrections. -The abbrev is created in the local dictionary unless the prefix P -argument is provided, when it's created in the global dictionary." + "Find and correct the previous misspelled word, creating an abbrev. + +Finds the nearest misspelled word before point, prompts for correction, +and creates an abbrev so the misspelling auto-expands in the future. + +With prefix argument P, the abbrev is created in the local abbrev table. +Without prefix argument, it's created in the global abbrev table. + +Press C-' repeatedly to step through misspellings one at a time." (interactive "P") - (unless (featurep 'files) - (require 'files)) - (setq save-abbrevs 'silently) - (flyspell-buffer) - (save-excursion - (let (misspelled-word corrected-word) - (while (setq misspelled-word - (cj/flyspell-goto-previous-misspelling (point))) - (call-interactively 'flyspell-correct-at-point) - (setq corrected-word (downcase (or (thing-at-point 'word) ""))) - (when (and misspelled-word corrected-word - (not (string= corrected-word misspelled-word))) - (message "\"%s\" now expands to \"%s\" %sally" - misspelled-word corrected-word (if p "loc" "glob")) - (define-abbrev - (if p local-abbrev-table global-abbrev-table) - misspelled-word corrected-word)) - (goto-char (point-min)))) - (message "Spell check complete."))) - -(define-key global-map (kbd "C-'") 'cj/flyspell-then-abbrev) + ;; Check if spell checker is available + (unless (or (executable-find "aspell") + (executable-find "ispell") + (executable-find "hunspell")) + (user-error "No spell checker found. Install aspell, ispell, or hunspell")) + + ;; Run flyspell-buffer only if buffer hasn't been checked yet + (unless (bound-and-true-p flyspell-mode) + (flyspell-buffer)) + + (let ((misspelled-word (cj/flyspell-goto-previous-misspelling (point)))) + (if (not misspelled-word) + (message "No misspellings found before point") + ;; Found a misspelling, correct it + (call-interactively 'flyspell-correct-at-point) + (let ((corrected-word (downcase (or (thing-at-point 'word) "")))) + (when (and misspelled-word corrected-word + (not (string= corrected-word misspelled-word))) + (message "\"%s\" now expands to \"%s\" %sally" + misspelled-word corrected-word (if p "loc" "glob")) + (define-abbrev + (if p local-abbrev-table global-abbrev-table) + misspelled-word corrected-word)))))) + +;; -------------------------------- Keybindings -------------------------------- +;; Global keybindings for spell checking commands +;; With autoload cookies, these will lazy-load the file when pressed + +;;;###autoload (keymap-set global-map "C-c f" #'cj/flyspell-toggle) +;;;###autoload (keymap-set global-map "C-'" #'cj/flyspell-then-abbrev) (provide 'flyspell-and-abbrev) ;;; flyspell-and-abbrev.el ends here. |
