From e3750dbf66896eb4a88aa39789e48f9cb5d690ab Mon Sep 17 00:00:00 2001 From: Henrik Lissner Date: Sun, 23 Aug 2020 18:48:50 -0400 Subject: [PATCH] checkers/spell: add +flyspell flag spell-fu lacks support for multiple dictionaries, affix expansion, and many non-English dictionaries, so I've added back flyspell support to the spell module, but opt-in, because it is still the significantly slower option and spell-fu may support them one day. If not, tlikonen/wcheck-mode is another alternative to consider. Fixes #3813 --- modules/checkers/spell/README.org | 144 ++++++++--- modules/checkers/spell/autoload/flyspell.el | 28 +++ .../{autoload.el => autoload/spell-fu.el} | 5 +- modules/checkers/spell/config.el | 225 ++++++++++++------ modules/checkers/spell/doctor.el | 6 +- modules/checkers/spell/packages.el | 10 +- modules/lang/markdown/config.el | 2 + modules/lang/ocaml/config.el | 4 - 8 files changed, 313 insertions(+), 111 deletions(-) create mode 100644 modules/checkers/spell/autoload/flyspell.el rename modules/checkers/spell/{autoload.el => autoload/spell-fu.el} (96%) diff --git a/modules/checkers/spell/README.org b/modules/checkers/spell/README.org index 528e03586..a3d9dc8dc 100644 --- a/modules/checkers/spell/README.org +++ b/modules/checkers/spell/README.org @@ -9,17 +9,23 @@ - [[#module-flags][Module Flags]] - [[#plugins][Plugins]] - [[#prerequisites][Prerequisites]] + - [[#aspell][Aspell]] + - [[#hunspell][Hunspell]] - [[#features][Features]] - [[#configuration][Configuration]] - [[#changing-how-quickly-spell-fu-spellchecks-after-changes][Changing how quickly spell-fu spellchecks after changes]] + - [[#spell-fu-users][Spell-fu users]] + - [[#flyspell-users][Flyspell users]] - [[#reducing-false-positives-by-disabling-spelling-on-certain-faces][Reducing false positives by disabling spelling on certain faces]] + - [[#spell-fu-users-1][Spell-fu users]] + - [[#flyspell-users-1][Flyspell users]] - [[#adding-or-removing-words-to-your-personal-dictionary][Adding or removing words to your personal dictionary]] - [[#troubleshooting][Troubleshooting]] * Description This modules provides spellchecking powered by =aspell= or =hunspell=. -Spellcheck is automatically loaded in all ~text-mode~ derivatives, which +Spellcheck is automatically loaded in many ~text-mode~ derivatives, which includes ~org-mode~, ~markdown-mode~, the Git Commit buffer (from magit), ~mu4e-compose-mode~, and others. @@ -27,63 +33,137 @@ includes ~org-mode~, ~markdown-mode~, the Git Commit buffer (from magit), This module has no dedicated maintainers. ** Module Flags ++ =+flyspell= Use =flyspell= instead of =spell-fu=. It's significantly slower, + but supports multiple languages and dictionaries. + =+aspell= Use =aspell= as a backend for correcting words. + =+hunspell= Use =hunspell= as a backend for correcting words. -+ =+everywhere= Use spellcheck in prog-mode and conf-mode derivatives as well as - text-mode. Basically, enable =spell-fu-mode= everywhere. ++ =+everywhere= Spell check in programming modes as well (in comments). ** Plugins -+ [[https://gitlab.com/ideasman42/emacs-spell-fu][spell-fu]] ++ if =+flyspell= + + [[https://github.com/d12frosted/flyspell-correct][flyspell-correct]] + + [[https://github.com/d12frosted/flyspell-correct#flyspell-correct-ivy-interface][flyspell-correct-ivy]] (=completion/ivy=) + + [[https://github.com/d12frosted/flyspell-correct#flyspell-correct-helm-interface][flyspell-correct-helm]] (=completion/helm=) + + [[https://github.com/d12frosted/flyspell-correct#flyspell-correct-popup-interface][flyspell-correct-popup]] (if *neither* =completion/ivy= or =completion/helm=) + + [[https://github.com/rolandwalker/flyspell-lazy][flyspell-lazy]] ++ else + + [[https://gitlab.com/ideasman42/emacs-spell-fu][spell-fu]] * Prerequisites -This module requires =aspell= to be installed, whether or not you intend to use -=hunspell= as the word correction backend. =spell-fu= does not yet support -generating the word list with a custom command or =hunspell=, but =hunspell= can -still be used to provide correction suggestions when invoking ~ispell-word~. +This module requires one of =aspell= or =hunspell= installed on your system and +in your ~PATH~. They also need dictionaries for your language(s). -You will need =hunspell= installed (via your OS package manager) to use it as a -correction backend. +#+begin_quote +If you *are not* using =+flyspell=, you will need aspell (and a dictionary) +installed whether or not you have =+hunspell= enabled. This is because +=spell-fu= does not support generating the word list with anything other than +=aspell= yet. +#+end_quote + +** Aspell ++ Ubuntu: ~apt-get install aspell aspell-en~ ++ Arch Linux: ~pacman -S aspell aspell-en~ ++ NixOS: + #+BEGIN_SRC nix + { + environment.systemPackages = with pkgs; [ + aspell + aspellDicts.en + aspellDicts.en-computers + aspellDicts.en-science + ]; + } + #+END_SRC + +** TODO Hunspell * Features -+ Spellchecking based on =aspell=. -+ Spell correction using =aspell= or =hunspell= (through ~M-x ispell-word~). ++ Spell checking and correction using =aspell= or =hunspell=. + Ignores source code inside org or markdown files. ++ Lazily spellchecking recent changes only when idle. ++ Choosing suggestions using completion interfaces (=ivy= or =helm=). -When using =+everywhere=, ~spell-fu-mode~ is activated for as many major modes -as possible, and not only ~text-mode~ derivatives. =spell-fu= will only spell -check comments in programming major modes. +When using =+everywhere=, spell checking is performed for as many major modes as +possible, and not only ~text-mode~ derivatives. e.g. in comments for programming +major modes. * Configuration Dictionary is set by =ispell-dictionary= variable. Can be changed locally with the function =ispell-change-dictionary=. ** Changing how quickly spell-fu spellchecks after changes -Adjust ~spell-fu-idle-delay~ to change how long spell-fu waits to spellcheck -after recent changes (its default value as ~0.25~). - +*** Spell-fu users +Adjust ~spell-fu-idle-delay~ to change how long Emacs waits to spellcheck after +recent changes. #+BEGIN_SRC elisp (after! spell-fu - (setq spell-fu-idle-delay 0.5)) + (setq spell-fu-idle-delay 0.5)) ; default is 0.25 +#+END_SRC + +*** Flyspell users +Lazy spellcheck is provided by =flyspell-lazy= package. + +=flyspell-lazy-idle-seconds= sets how many idle seconds until spellchecking +recent changes (default as 1), while =flyspell-lazy-window-idle-seconds= sets +how many seconds until the whole window is spellchecked (default as 3). +#+BEGIN_SRC elisp +(after! flyspell + (setq flyspell-lazy-idle-seconds 2)) #+END_SRC ** Reducing false positives by disabling spelling on certain faces -Exclude what faces to not preform spellchecking on by setting -~spell-fu-faces-exclude~ in a buffer-local hook: +*** Spell-fu users +Users can exclude what faces to preform spellchecking on by adjusting +~+spell-excluded-faces-alist~ in a buffer-local hook: #+BEGIN_SRC elisp -(setq-hook! 'markdown-mode-hook - spell-fu-faces-exclude '(markdown-code-face - markdown-reference-face - markdown-link-face - markdown-url-face - markdown-markup-face - markdown-html-attr-value-face - markdown-html-attr-name-face - markdown-html-tag-name-face)) +(setf (alist-get 'markdown-mode +spell-excluded-faces-alist) + '(markdown-code-face + markdown-reference-face + markdown-link-face + markdown-url-face + markdown-markup-face + markdown-html-attr-value-face + markdown-html-attr-name-face + markdown-html-tag-name-face)) +#+END_SRC + +*** Flyspell users +Flyspell will run a series of predicate functions to determine if a word should be spell checked. You can add your own with ~set-flyspell-predicate!~: +#+BEGIN_SRC elisp +(set-flyspell-predicate! '(markdown-mode gfm-mode) + #'+markdown-flyspell-word-p) +#+END_SRC + +Flyspell predicates take no arguments and must return a boolean to determine if +the word at point should be spell checked. For example: +#+BEGIN_SRC elisp +(defun +markdown-flyspell-word-p () + "Return t if point is on a word that should be spell checked. + +Return nil if on a link url, markup, html, or references." + (let ((faces (doom-enlist (get-text-property (point) 'face)))) + (or (and (memq 'font-lock-comment-face faces) + (memq 'markdown-code-face faces)) + (not (cl-loop with unsafe-faces = '(markdown-reference-face + markdown-url-face + markdown-markup-face + markdown-comment-face + markdown-html-attr-name-face + markdown-html-attr-value-face + markdown-html-tag-name-face + markdown-code-face) + for face in faces + if (memq face unsafe-faces) + return t))))) #+END_SRC ** Adding or removing words to your personal dictionary -Use ~M-x spell-fu-word-add~ and ~M-x spell-fu-word-remove~ to whitelist words -that you know are not misspellings. +Spell-fu users can call ~M-x spell-fu-word-add~ and ~M-x spell-fu-word-remove~ +to whitelist words that you know are not misspellings. These are on =zq= and +=zw=, respectively, for users with Evil enabled. + +Neither ispell nor =+flyspell= support managing personal dictionaries within +Emacs. * TODO Troubleshooting diff --git a/modules/checkers/spell/autoload/flyspell.el b/modules/checkers/spell/autoload/flyspell.el new file mode 100644 index 000000000..24abd42e1 --- /dev/null +++ b/modules/checkers/spell/autoload/flyspell.el @@ -0,0 +1,28 @@ +;;; checkers/spell/autoload/flyspell.el -*- lexical-binding: t; -*- +;;;###if (featurep! +flyspell) + +;;;###autodef +(defalias 'flyspell-mode! #'flyspell-mode) + +(defvar +spell--flyspell-predicate-alist nil + "TODO") + +;;;###autodef +(defun set-flyspell-predicate! (modes predicate) + "TODO" + (declare (indent defun)) + (dolist (mode (doom-enlist modes) +spell--flyspell-predicate-alist) + (add-to-list '+spell--flyspell-predicate-alist (cons mode predicate)))) + +;;;###autoload +(defun +spell-init-flyspell-predicate-h () + "TODO" + (when-let (pred (assq major-mode +spell--flyspell-predicate-alist)) + (setq-local flyspell-generic-check-word-predicate (cdr pred)))) + +;;;###autoload +(defun +spell-correction-at-point-p (&optional point) + "TODO" + (cl-loop for ov in (overlays-at (or point (point))) + if (overlay-get ov 'flyspell-overlay) + return t)) diff --git a/modules/checkers/spell/autoload.el b/modules/checkers/spell/autoload/spell-fu.el similarity index 96% rename from modules/checkers/spell/autoload.el rename to modules/checkers/spell/autoload/spell-fu.el index ddd5dbfdf..86c4bfde4 100644 --- a/modules/checkers/spell/autoload.el +++ b/modules/checkers/spell/autoload/spell-fu.el @@ -1,4 +1,5 @@ -;;; checkers/spell/autoload/ivy.el -*- lexical-binding: t; -*- +;;; checkers/spell/autoload/spell-fu.el -*- lexical-binding: t; -*- +;;;###if (not (featurep! +flyspell)) (defun +spell--correct (replace poss word orig-pt start end) (cond ((eq replace 'ignore) @@ -66,8 +67,6 @@ (let ((word (thing-at-point 'word t)) (orig-pt (point)) poss ispell-filter) - (save-current-buffer - (ispell-accept-buffer-local-defs)) (ispell-send-string "%\n") (ispell-send-string (concat "^" word "\n")) (while (progn (accept-process-output ispell-process) diff --git a/modules/checkers/spell/config.el b/modules/checkers/spell/config.el index 92051f7f8..f5668d1c5 100644 --- a/modules/checkers/spell/config.el +++ b/modules/checkers/spell/config.el @@ -1,50 +1,11 @@ ;;; checkers/spell/config.el -*- lexical-binding: t; -*- -(defvar +spell-correct-interface - (cond ((featurep! :completion ivy) - #'+spell-correct-ivy-fn) - ((featurep! :completion helm) - #'+spell-correct-helm-fn) - (#'+spell-correct-generic-fn)) - "Function to use to display corrections.") - -(defvar +spell-excluded-faces-alist - '((markdown-mode - . (markdown-code-face - markdown-html-attr-name-face - markdown-html-attr-value-face - markdown-html-tag-name-face - markdown-link-face - markdown-markup-face - markdown-reference-face - markdown-url-face)) - (org-mode - . (org-block - org-block-begin-line - org-block-end-line - org-code - org-date - org-formula - org-latex-and-related - org-link - org-meta-line - org-property-value - org-ref-cite-face - org-special-keyword - org-tag - org-todo - org-todo-keyword-done - org-todo-keyword-habt - org-todo-keyword-kill - org-todo-keyword-outd - org-todo-keyword-todo - org-todo-keyword-wait - org-verbatim))) - "Faces in certain major modes that spell-fu will not spellcheck.") - - ;; -;;; Packages +;;; Ispell + +;; `elisp-mode' is loaded at startup. In order to lazy load its config we need +;; to pretend it isn't loaded +(delq! 'ispell features) (global-set-key [remap ispell-word] #'+spell/correct) @@ -100,30 +61,156 @@ (_ (doom-log "Spell checker not found. Either install `aspell' or `hunspell'")))) -(use-package! spell-fu - :when (executable-find "aspell") - :hook (text-mode . spell-fu-mode) - :init - (setq spell-fu-directory (concat doom-etc-dir "spell-fu")) - (when (featurep! +everywhere) - (add-hook! '(yaml-mode-hook - conf-mode-hook - prog-mode-hook) - #'spell-fu-mode)) - :config - (defadvice! +spell--create-word-dict-a (_word words-file _action) - :before #'spell-fu--word-add-or-remove - (unless (file-exists-p words-file) - (make-directory (file-name-directory words-file) t) - (with-temp-file words-file - (insert (format "personal_ws-1.1 %s 0\n" ispell-dictionary))))) +;; +;;; Implementations - (add-hook! 'spell-fu-mode-hook - (defun +spell-init-excluded-faces-h () - "Set `spell-fu-faces-exclude' according to `+spell-excluded-faces-alist'." - (when-let (excluded (alist-get major-mode +spell-excluded-faces-alist)) - (setq-local spell-fu-faces-exclude excluded)))) +(if! (not (featurep! +flyspell)) - ;; TODO custom `spell-fu-check-range' function to exclude URLs from links or - ;; org-src blocks more intelligently. - ) + (use-package! spell-fu + :when (executable-find "aspell") + :hook (text-mode . spell-fu-mode) + :general ([remap ispell-word] #'+spell/correct) + :init + (defvar +spell-correct-interface + (cond ((featurep! :completion ivy) + #'+spell-correct-ivy-fn) + ((featurep! :completion helm) + #'+spell-correct-helm-fn) + (#'+spell-correct-generic-fn)) + "Function to use to display corrections.") + + (defvar +spell-excluded-faces-alist + '((markdown-mode + . (markdown-code-face + markdown-html-attr-name-face + markdown-html-attr-value-face + markdown-html-tag-name-face + markdown-link-face + markdown-markup-face + markdown-reference-face + markdown-url-face)) + (org-mode + . (org-block + org-block-begin-line + org-block-end-line + org-code + org-date + org-formula + org-latex-and-related + org-link + org-meta-line + org-property-value + org-ref-cite-face + org-special-keyword + org-tag + org-todo + org-todo-keyword-done + org-todo-keyword-habt + org-todo-keyword-kill + org-todo-keyword-outd + org-todo-keyword-todo + org-todo-keyword-wait + org-verbatim))) + "Faces in certain major modes that spell-fu will not spellcheck.") + + (setq spell-fu-directory (concat doom-etc-dir "spell-fu")) + (when (featurep! +everywhere) + (add-hook! '(yaml-mode-hook + conf-mode-hook + prog-mode-hook) + #'spell-fu-mode)) + :config + (map! :after spell-fu + :map override + :n [return] + (cmds! (memq 'spell-fu-incorrect-face (face-at-point nil t)) + #'+spell/correct)) + + (defadvice! +spell--create-word-dict-a (_word words-file _action) + :before #'spell-fu--word-add-or-remove + (unless (file-exists-p words-file) + (make-directory (file-name-directory words-file) t) + (with-temp-file words-file + (insert (format "personal_ws-1.1 %s 0\n" ispell-dictionary))))) + + (add-hook! 'spell-fu-mode-hook + (defun +spell-init-excluded-faces-h () + "Set `spell-fu-faces-exclude' according to `+spell-excluded-faces-alist'." + (when-let (excluded (alist-get major-mode +spell-excluded-faces-alist)) + (setq-local spell-fu-faces-exclude excluded)))) + + ;; TODO custom `spell-fu-check-range' function to reduce false positives + ;; more intelligently, or modify `spell-fu-word-regexp' to include + ;; non-latin charactersets. + ) + + (use-package! flyspell ; built-in + :defer t + :preface + ;; `flyspell' is loaded at startup. In order to lazy load its config we need + ;; to pretend it isn't loaded. + (defer-feature! flyspell flyspell-mode flyspell-prog-mode) + :init + (add-hook! '(org-mode-hook + markdown-mode-hook + TeX-mode-hook + rst-mode-hook + mu4e-compose-mode-hook + message-mode-hook + git-commit-mode-hook) + #'flyspell-mode) + + (when (featurep! +everywhere) + (add-hook! '(yaml-mode-hook + conf-mode-hook + prog-mode-hook) + #'flyspell-prog-mode)) + + :config + (setq flyspell-issue-welcome-flag nil + ;; Significantly speeds up flyspell, which would otherwise print + ;; messages for every word when checking the entire buffer + flyspell-issue-message-flag nil) + + (add-hook! 'flyspell-mode-hook + (defun +spell-inhibit-duplicate-detection-maybe-h () + "Don't mark duplicates when style/grammar linters are present. +e.g. proselint and langtool." + (and (or (and (bound-and-true-p flycheck-mode) + (executable-find "proselint")) + (featurep 'langtool)) + (setq-local flyspell-mark-duplications-flag nil)))) + + ;; Ensure mode-local predicates declared with `set-flyspell-predicate!' are + ;; used in their respective major modes. + (add-hook 'flyspell-mode-hook #'+spell-init-flyspell-predicate-h) + + (let ((flyspell-correct + (cmds! (and (not (or mark-active (ignore-errors (evil-insert-state-p)))) + (memq 'flyspell-incorrect (face-at-point nil t))) + #'flyspell-correct-at-point))) + (map! :map flyspell-mouse-map + "RET" flyspell-correct + [return] flyspell-correct + [mouse-1] #'flyspell-correct-at-point))) + + + (use-package! flyspell-correct + :commands flyspell-correct-previous + :general ([remap ispell-word] #'flyspell-correct-at-point) + :config + (cond ((and (featurep! :completion helm) + (require 'flyspell-correct-helm nil t))) + ((and (featurep! :completion ivy) + (require 'flyspell-correct-ivy nil t))) + ((require 'flyspell-correct-popup nil t) + (setq flyspell-popup-correct-delay 0.8) + (define-key popup-menu-keymap [escape] #'keyboard-quit)))) + + + (use-package! flyspell-lazy + :after flyspell + :config + (setq flyspell-lazy-idle-seconds 1 + flyspell-lazy-window-idle-seconds 3) + (flyspell-lazy-mode +1))) diff --git a/modules/checkers/spell/doctor.el b/modules/checkers/spell/doctor.el index e35d6517f..b24824ebb 100644 --- a/modules/checkers/spell/doctor.el +++ b/modules/checkers/spell/doctor.el @@ -1,7 +1,9 @@ ;;; checkers/spell/doctor.el -*- lexical-binding: t; -*- -(unless (executable-find "aspell") - (warn! "Couldn't find aspell executable; spell checker will not work")) +(when (or (not (featurep! +flyspell)) + (featurep! +aspell)) + (unless (executable-find "aspell") + (warn! "Couldn't find aspell executable; spell checker will not work"))) (when (featurep! +hunspell) (unless (executable-find "hunspell") diff --git a/modules/checkers/spell/packages.el b/modules/checkers/spell/packages.el index 595724a29..875e9eea7 100644 --- a/modules/checkers/spell/packages.el +++ b/modules/checkers/spell/packages.el @@ -1,4 +1,12 @@ ;; -*- no-byte-compile: t; -*- ;;; checkers/spell/packages.el -(package! spell-fu :pin "e94d01cdc822e02968971cde09276047a5d55772") +(if (not (featurep! +flyspell)) + (package! spell-fu :pin "e94d01cdc822e02968971cde09276047a5d55772") + (package! flyspell-correct :pin "dea1290a371c540dde7b8d0eef7a12d92f7a0b83") + (cond ((featurep! :completion ivy) + (package! flyspell-correct-ivy)) + ((featurep! :completion helm) + (package! flyspell-correct-helm)) + ((package! flyspell-correct-popup))) + (package! flyspell-lazy :pin "3ebf68cc9eb10c972a2de8d7861cbabbbce69570")) diff --git a/modules/lang/markdown/config.el b/modules/lang/markdown/config.el index 9f50dcd80..d82e396db 100644 --- a/modules/lang/markdown/config.el +++ b/modules/lang/markdown/config.el @@ -53,6 +53,8 @@ capture, the end position, and the output buffer.") (add-to-list 'org-src-lang-modes '("md" . markdown))) :config + (set-flyspell-predicate! '(markdown-mode gfm-mode) + #'+markdown-flyspell-word-p) (set-lookup-handlers! '(markdown-mode gfm-mode) ;; `markdown-follow-thing-at-point' may open an external program or a ;; buffer. No good way to tell, so pretend it's async. diff --git a/modules/lang/ocaml/config.el b/modules/lang/ocaml/config.el index e5a1e709d..60c19ade3 100644 --- a/modules/lang/ocaml/config.el +++ b/modules/lang/ocaml/config.el @@ -20,10 +20,6 @@ (setq tuareg-opam-insinuate t) (tuareg-opam-update-env (tuareg-opam-current-compiler)) - ;; Spell-check comments - (when (featurep! :checkers spell) - (add-hook 'tuareg-mode-local-vars-hook #'flyspell-prog-mode)) - (setq-hook! 'tuareg-mode-hook comment-line-break-function #'+ocaml/comment-indent-new-line)