branch: externals/greader
commit df4f764921bc89a219ee770e2eb1348dccf93821
Author: Michelangelo Rodriguez <[email protected]>
Commit: Michelangelo Rodriguez <[email protected]>
fix(dict): Fix and complete dictionary merging implementation
* greader-dict.el (greader-dict--merge): Return KEY unchanged when
already merged, instead of nil; fixes nil key being inserted into
the hash table on duplicate merge attempts.
(greader-dict-merge--check-candidate): Fix inverted predicate logic
and broken path comparison (full path vs. nondirectory); use
expand-file-name for correct equality check.
(greader-dict-merge--dictionaries): Iterate over (cdr dictionaries)
to skip the main dictionary itself; call greader-dict-read-from-dict-file
directly to avoid circular dispatch through greader-dict-merge-dictionary.
(greader-dict-merge-dictionary): When called with FILENAME, update
greader-dict-merge-dictionaries-alist, pass merge=t to
greader-dict-read-from-dict-file, and respect greader-dict-merge-save.
(greader-dict-merge-save): Rename to greader-dict-merge-save-to-file
to avoid defcustom/defun namespace collision.
(greader-dict-mode): Call greader-dict-load-merges and
greader-dict-merge--dictionaries on startup to auto-reload saved merges.
(greader-dict-prefix-map): Bind "M" to greader-dict-merge-dictionary.
(greader-dict-filters-mode): Add forward declaration before
with-greader-dict-temp-buffer to suppress byte-compiler warning.
* greader-dict-tests.el: New file; 7 ERT tests covering
greader-dict--merged-p, greader-dict--merge, greader-dict-add with
merge flag, greader-dict-write-file skipping merged entries, and
greader-dict-merge-dictionary loading and updating the alist.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
greader-dict-tests.el | 112 ++++++++++++++++++++++++++++++++++++++++++++
greader-dict.el | 125 +++++++++++++++++++++++++++++++++-----------------
2 files changed, 196 insertions(+), 41 deletions(-)
diff --git a/greader-dict-tests.el b/greader-dict-tests.el
new file mode 100644
index 0000000000..c7cd227d74
--- /dev/null
+++ b/greader-dict-tests.el
@@ -0,0 +1,112 @@
+;; -*- lexical-binding: t; -*-
+(require 'ert)
+(require 'greader-dict)
+
+;;; Helpers
+
+(defmacro with-greader-dict-test-buffer (&rest body)
+ "Run BODY in a buffer with `greader-dict-mode' initialized."
+ (declare (indent defun))
+ `(with-temp-buffer
+ (greader-dict-mode 1)
+ ,@body))
+
+;;; greader-dict--merge / greader-dict--merged-p
+
+(ert-deftest greader-dict--merge-sets-property ()
+ "greader-dict--merge returns a key with the merged text property set."
+ (let ((key (greader-dict--merge "foo")))
+ (should (greader-dict--merged-p key))))
+
+(ert-deftest greader-dict--merge-idempotent ()
+ "greader-dict--merge on an already-merged key returns the same key."
+ (let* ((key1 (greader-dict--merge "foo"))
+ (key2 (greader-dict--merge key1)))
+ (should (greader-dict--merged-p key2))
+ (should (equal (substring-no-properties key1)
+ (substring-no-properties key2)))))
+
+(ert-deftest greader-dict--merged-p-nil-for-plain-string ()
+ "greader-dict--merged-p returns nil for a plain string."
+ (should-not (greader-dict--merged-p "foo")))
+
+;;; greader-dict-add with merge=t
+
+(ert-deftest greader-dict-add-merge-marks-entry ()
+ "greader-dict-add with MERGE non-nil marks the hash key as merged."
+ (with-greader-dict-test-buffer
+ (greader-dict-add "testword" "sostituzione" t)
+ (let ((found nil))
+ (maphash (lambda (k _v)
+ (when (equal (substring-no-properties k) "testword")
+ (setq found k)))
+ greader-dictionary)
+ (should found)
+ (should (greader-dict--merged-p found)))))
+
+(ert-deftest greader-dict-add-no-merge-does-not-mark ()
+ "greader-dict-add without MERGE does not mark the key as merged."
+ (with-greader-dict-test-buffer
+ (greader-dict-add "testword2" "sostituzione")
+ (let ((found nil))
+ (maphash (lambda (k _v)
+ (when (equal (substring-no-properties k) "testword2")
+ (setq found k)))
+ greader-dictionary)
+ (should found)
+ (should-not (greader-dict--merged-p found)))))
+
+;;; greader-dict-write-file skips merged entries
+
+(ert-deftest greader-dict-write-file-skips-merged ()
+ "Merged entries are not written to the dictionary file.
+Uses `cl-letf' to intercept `write-region' rather than touching the
+filesystem, avoiding the `greader-dict-directory' path-rewriting done
+by `greader-dict--get-file-name'."
+ (require 'cl-lib)
+ (let (written-content)
+ (cl-letf (((symbol-function 'write-region)
+ (lambda (start end _filename &rest _)
+ (setq written-content (buffer-substring start end)))))
+ (with-temp-buffer
+ (setq greader-dict--current-reading-buffer (current-buffer))
+ (setq greader-dictionary (make-hash-table :test 'ignore-case))
+ (greader-dict-add "parola" "sostituzione") ; normal entry
+ (greader-dict-add "fusa" "merged-value" t) ; merged entry
+ (let ((greader-dict--saved-flag nil))
+ (greader-dict-write-file))))
+ (should written-content)
+ (should (string-match-p "parola" written-content))
+ (should-not (string-match-p "fusa" written-content))))
+
+;;; greader-dict-merge-dictionary loads entries as merged and updates alist
+
+(ert-deftest greader-dict-merge-dictionary-loads-and-marks ()
+ "greader-dict-merge-dictionary loads entries from file marked as merged."
+ (let ((aux-file (make-temp-file "greader-dict-aux" nil ".dict")))
+ (unwind-protect
+ (progn
+ (write-region "\"parola\"=sostituzione\n" nil aux-file)
+ (with-temp-buffer
+ (setq greader-dict--current-reading-buffer (current-buffer))
+ (setq greader-dictionary (make-hash-table :test 'ignore-case))
+ (setq greader-dict-merge-dictionaries-alist nil)
+ (let* ((main-file (greader-dict--get-file-name))
+ (greader-dict-merge-save nil))
+ (greader-dict-merge-dictionary aux-file)
+ ;; Entry must be present and marked merged.
+ (let ((found nil))
+ (maphash (lambda (k _v)
+ (when (equal (substring-no-properties k) "parola")
+ (setq found k)))
+ greader-dictionary)
+ (should found)
+ (should (greader-dict--merged-p found)))
+ ;; Alist must record the merge.
+ (let ((entry (assoc main-file
greader-dict-merge-dictionaries-alist)))
+ (should entry)
+ (should (member aux-file (cdr entry)))))))
+ (when (file-exists-p aux-file)
+ (delete-file aux-file)))))
+
+(provide 'greader-dict-tests)
diff --git a/greader-dict.el b/greader-dict.el
index 098b16df12..9fa0558315 100644
--- a/greader-dict.el
+++ b/greader-dict.el
@@ -116,6 +116,32 @@
;; This module offers a command
;; `greader-dict-pronounce-in-other-language' that can be used for
;; earing how the tts pronounces a word in another language.
+;;
+;; Dictionary merging
+;;
+;; You can merge one or more auxiliary dictionaries into the current one
+;; using `greader-dict-merge-dictionary' (C-r d M).
+;; Merged entries are loaded into memory and applied during reading, but
+;; are never written back to the main dictionary file. This means that
+;; the original dictionaries remain independent: any change to the main
+;; dictionary will not affect the auxiliary ones, and vice versa.
+;;
+;; Merge configurations can be made persistent across Emacs sessions.
+;; The behaviour is controlled by the customizable variable
+;; `greader-dict-merge-save':
+;; t - always save merge configurations automatically.
+;; ask - ask whether to save after each merge (the default).
+;; nil - never save; merges are lost when Emacs exits.
+;;
+;; Saved merge configurations are stored in the file pointed to by
+;; `greader-dict-merge-file' (default: `.merges' in the dictionary
+;; directory). When `greader-dict-mode' is enabled, any previously
+;; saved merge configurations are loaded and applied automatically.
+;;
+;; You can merge additional dictionaries at any time by calling
+;; `greader-dict-merge-dictionary' again. Calling it without an
+;; argument re-applies all auxiliary dictionaries currently recorded for
+;; the active dictionary.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;;; Change Log:
@@ -189,6 +215,7 @@
(define-key greader-dict-prefix-map "l"
#'greader-dict-pronounce-in-other-language)
(define-key greader-dict-prefix-map "m" #'greader-dict-modify-key)
(define-key greader-dict-prefix-map "s" #'greader-dict-save)
+(define-key greader-dict-prefix-map "M" #'greader-dict-merge-dictionary)
(define-key greader-dict-prefix-map "f" greader-dict-filters-prefix-map)
(define-key greader-dict-filters-prefix-map "a" #'greader-dict-filter-add)
@@ -204,6 +231,8 @@
(defvar greader-dict-filter-indicator "%f")
+(defvar greader-dict-filters-mode)
+
;; This macro calls `with-temp-buffer', setting all the necessary
;; local variables to useful values. This means that all sensible
;; variables will be bound to `greader-dict-current-buffer' local
@@ -245,39 +274,41 @@ buffer."
(get-text-property 0 'greader-dict-merged key))
(defun greader-dict--merge (key)
- "Set KEY as merged."
- (unless (greader-dict--merged-p key)
+ "Return KEY with the `greader-dict-merged' text property set."
+ (if (greader-dict--merged-p key)
+ key
(propertize key 'greader-dict-merged t)))
;; merge customization
(defvar greader-dict-merge-dictionaries-alist ()
- "An alist containing merged dictionaries.
-The car of this alist should be the file name of the main dictionary.
-The cdr is a list of the auxiliary files, that is, the dictionaries to
-merge.
-these files should be located in the same directory.
-This is to hobey that you accidentally mix dictionaries for different
+ "Alist of merged dictionaries.
+Each entry has the form (MAIN-DICT AUX1 AUX2 ...) where MAIN-DICT is
+the absolute path of the main dictionary and each AUXn is the path of
+an auxiliary dictionary merged into it.
+Auxiliary files should reside in the same directory as the main
+dictionary; this prevents accidentally mixing dictionaries for different
languages.
-The command `greader-dict-merge' is an interface that takes in account
-these restrictions, so feel free to use it.")
+Use `greader-dict-merge-dictionary' to add entries to this list.")
(defcustom greader-dict-merge-save 'ask
- "If t, makes your merges persistent against emacs sessions.
-If it is `ask', then ask if you want to save your merges.
-nil means don't save at all."
- :type '(choice (const :tag "alwais save" t)
- (const :tag "ask every time" ask)
+ "Controls persistence of merge configurations across Emacs sessions.
+If t, merge configurations are saved automatically after each merge.
+If `ask', you are prompted whether to save after each merge.
+If nil, merge configurations are never saved."
+ :type '(choice (const :tag "Always save" t)
+ (const :tag "Ask every time" ask)
(const :tag "Do not save at all" nil)))
(defcustom greader-dict-merge-file (file-name-concat
greader-dict-directory ".merges")
- "File in which save the merges."
+ "File used to persist merge configurations.
+The file stores `greader-dict-merge-dictionaries-alist' so that merges
+survive Emacs restarts. See also `greader-dict-merge-save'."
:type '(file))
-(defun greader-dict-merge-save (&optional filename)
- "Save the variable `greader-dict-merge-save' in FILENAME.
-If filename is nil or not specified, use
-`greader-dict-merge-file'."
+(defun greader-dict-merge-save-to-file (&optional filename)
+ "Save `greader-dict-merge-dictionaries-alist' to FILENAME.
+If FILENAME is nil, use `greader-dict-merge-file'."
(unless filename
(setq filename greader-dict-merge-file))
(with-greader-dict-temp-buffer
@@ -308,34 +339,44 @@ exist or some other system errors."
nil)))
(defun greader-dict-merge--check-candidate (filename)
- "Predicate for `read-file-name."
- (if (equal filename (file-name-nondirectory
- (greader-dict--get-file-name)))
- t
- nil))
+ "Predicate for `read-file-name'.
+Return t for every file except the current dictionary itself."
+ (not (equal (expand-file-name filename)
+ (expand-file-name (greader-dict--get-file-name)))))
-(declare-function 'greader-dict-merge-dictionary nil)
(defun greader-dict-merge--dictionaries ()
- "Merge the current dictionary with the contents of
-`greader-dict-merge-dictionaries-alist."
- (if-let* ((dictionaries (greader-dict-merge-get)))
- (dolist (dictionary dictionaries)
- (greader-dict-merge-dictionary dictionary))
- nil))
+ "Merge auxiliary dictionaries listed for the current dictionary.
+Reads all files in the cdr of the matching entry in
+`greader-dict-merge-dictionaries-alist', marking every loaded entry
+as merged so it is never written back to the main dictionary file."
+ (when-let* ((dictionaries (greader-dict-merge-get)))
+ (dolist (dictionary (cdr dictionaries))
+ (greader-dict-read-from-dict-file t dictionary t))))
(defun greader-dict-merge-dictionary (&optional filename)
- "Merge contents of FILENAME in `greader-dictionary'.
-If filename is nil, merge only using
- `greader-dict-merge-dictionaries-alist'."
+ "Merge contents of FILENAME into `greader-dictionary'.
+FILENAME entries are marked as merged and are never saved to the main
+dictionary file.
+If FILENAME is nil, re-merge all auxiliaries already recorded in
+`greader-dict-merge-dictionaries-alist' for the current dictionary."
(interactive
- (let ((result
- (read-file-name "Dictionary file to merge: " greader-dict-directory
nil t nil
- 'greader-dict-merge--check-candidate)))
- (list result)))
+ (list
+ (read-file-name "Dictionary file to merge: " greader-dict-directory nil t
nil
+ #'greader-dict-merge--check-candidate)))
(if (null filename)
(greader-dict-merge--dictionaries)
- (greader-dict-merge--dictionaries)
- (greader-dict-read-from-dict-file filename)))
+ (let ((main (greader-dict--get-file-name)))
+ (if-let* ((entry (assoc main greader-dict-merge-dictionaries-alist)))
+ (unless (member filename (cdr entry))
+ (setcdr entry (append (cdr entry) (list filename))))
+ (push (list main filename) greader-dict-merge-dictionaries-alist)))
+ (greader-dict-read-from-dict-file t filename t)
+ (cond
+ ((eq greader-dict-merge-save t)
+ (greader-dict-merge-save-to-file))
+ ((eq greader-dict-merge-save 'ask)
+ (when (y-or-n-p "Save merge configuration? ")
+ (greader-dict-merge-save-to-file))))))
(defvar-keymap greader-dict-filter-map
:doc "key bindings for greader-dict filter feature."
@@ -418,6 +459,8 @@ as a word definition."
(setq greader-dictionary (make-hash-table :test 'ignore-case))
(setq greader-dict--current-reading-buffer (current-buffer))
(greader-dict-read-from-dict-file)
+ (greader-dict-load-merges)
+ (greader-dict-merge--dictionaries)
(add-hook 'greader-after-get-sentence-functions
#'greader-dict--replace-wrapper 1)
(add-hook 'buffer-list-update-hook #'greader-dict--update)