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)

Reply via email to