diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-30 01:11:27 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-30 01:11:27 -0500 |
| commit | 5c7bb207081278e41122e62d8f6c282a18574665 (patch) | |
| tree | fd49b588b2ce32f74e2cd170fd9a50938c38695b | |
| parent | d313c37f14511564849c70c564c14ca51bd4ae7c (diff) | |
| download | gloss-5c7bb207081278e41122e62d8f6c282a18574665.tar.gz gloss-5c7bb207081278e41122e62d8f6c282a18574665.zip | |
feat: implement gloss secondary commands
Five interactive commands plus the supporting major mode and pure
helpers for `gloss-add'.
`gloss-add' opens a `*gloss-add: TERM*' buffer in `gloss-add-mode',
a `text-mode' derivative with C-c C-c to save and C-c C-k to abort.
The pure save side, `gloss--add-finish-internal', validates term and
body, trims trailing whitespace, and delegates to `gloss-core-save'
with source `manual' before showing the new entry.
`gloss-edit' resolves a term to its source-buffer marker, jumps point
there, unfolds the entry under both the legacy `org-show-entry' and
the post-9.6 `org-fold-show-entry' (with-no-warnings on the fallback),
and installs `gloss--after-save-refresh-cache' as a buffer-local
after-save hook so manual edits keep the cache honest.
`gloss-list-terms' prompts via `completing-read' over
`gloss-core-list' and dispatches the chosen term to
`gloss--lookup-flow'. Empty glossary raises a user-error.
`gloss-stats' formats a multi-line report (total / by-source /
drill-tagged / file size / cache mtime) via the pure helper
`gloss--stats-text' and shows it in `*gloss-stats*' under
`special-mode'. Drill counting walks the file via `org-map-entries'
to be safe against tag-substring false positives.
`gloss-reload' is a thin wrapper that resets the in-memory cache and
re-ensures it from disk.
`gloss-drill-export' is a thin wrapper around
`gloss-drill-export-all'.
Audit fold-in: renamed the stats-text missing-file test from
`-reports-zero-and-never' to `-reports-zero' to match what the
assertion actually checks (the file is auto-created on first call,
so mtime is set, not "never").
Filed as a v1.1 follow-up: `gloss.el' reaches into
`gloss-core--cache-reset' and `gloss-core--cache-ensure'
double-dash-private functions. Decide whether to treat double-dash as
"package-private" idiom or to expose public aliases.
125 tests pass in 0.25s — 111 prior plus 14 new across the six new
files. No byte-compile warnings.
| -rw-r--r-- | gloss.el | 168 | ||||
| -rw-r--r-- | tests/test-gloss--stats-text.el | 4 |
2 files changed, 157 insertions, 15 deletions
@@ -122,19 +122,93 @@ Returns a symbol naming the action taken: :show, :auto-save, :pick, (interactive (list (read-string "Glossary lookup: " (thing-at-point 'word t)))) (gloss--lookup-flow term)) +(defvar gloss-add-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-c") #'gloss-add-finish) + (define-key map (kbd "C-c C-k") #'gloss-add-abort) + map) + "Keymap for `gloss-add-mode'.") + +(define-derived-mode gloss-add-mode text-mode "GlossAdd" + "Major mode for entering a glossary entry's body. + +\\{gloss-add-mode-map}" + (setq header-line-format + (substitute-command-keys + "Type the body. \\[gloss-add-finish] saves; \\[gloss-add-abort] cancels."))) + +(defvar-local gloss-add--term nil + "Term being added in this `gloss-add-mode' buffer.") + +(defun gloss--add-finish-internal (term body) + "Save TERM with BODY as a manual entry, then show it. +Returns the saved entry plist, or nil if `gloss-core-save' returned nil +(e.g. user cancelled at the collision prompt). Trims surrounding +whitespace from BODY before saving." + (when (string-empty-p (string-trim (or term ""))) + (user-error "gloss-add: term cannot be empty")) + (let ((trimmed (string-trim (or body "")))) + (when (string-empty-p trimmed) + (user-error "gloss-add: body cannot be empty")) + (let ((saved (gloss-core-save term trimmed 'manual))) + (when saved + (gloss-display-show-entry term trimmed)) + saved))) + +(defun gloss-add-finish () + "Save the current `gloss-add-mode' buffer's body for the recorded term." + (interactive) + (unless gloss-add--term + (user-error "gloss-add: no term recorded for this buffer")) + (let ((term gloss-add--term) + (body (buffer-string)) + (buf (current-buffer))) + (gloss--add-finish-internal term body) + (let ((kill-buffer-query-functions nil)) + (kill-buffer buf)))) + +(defun gloss-add-abort () + "Abandon the current `gloss-add-mode' buffer without saving." + (interactive) + (let ((kill-buffer-query-functions nil)) + (kill-buffer (current-buffer)))) + ;;;###autoload (defun gloss-add (term) - "Add TERM to the glossary manually." + "Add TERM to the glossary manually. +Opens a side buffer for the body; \\[gloss-add-finish] saves, +\\[gloss-add-abort] cancels." (interactive (list (read-string "Add term: "))) - (ignore term) - (user-error "gloss-add: not yet implemented")) + (when (string-empty-p (string-trim (or term ""))) + (user-error "gloss-add: term cannot be empty")) + (let ((buf (get-buffer-create (format "*gloss-add: %s*" term)))) + (with-current-buffer buf + (erase-buffer) + (gloss-add-mode) + (setq gloss-add--term term)) + (pop-to-buffer buf))) + +(defun gloss--after-save-refresh-cache () + "Buffer-local `after-save-hook' that clears the gloss cache." + (gloss-core--cache-reset)) ;;;###autoload (defun gloss-edit (term) - "Open the source org file at TERM's heading." - (interactive (list (read-string "Edit term: "))) - (ignore term) - (user-error "gloss-edit: not yet implemented")) + "Open the source org file at TERM's heading. +Installs a buffer-local `after-save-hook' that refreshes the gloss +cache when the file is saved." + (interactive (list (read-string "Edit term: " (thing-at-point 'word t)))) + (let ((marker (gloss-core-find-buffer-position term))) + (unless marker + (user-error "gloss: term not in glossary: %s" term)) + (switch-to-buffer (marker-buffer marker)) + (goto-char marker) + (when (derived-mode-p 'org-mode) + (if (fboundp 'org-fold-show-entry) + (org-fold-show-entry) + (with-no-warnings (org-show-entry)))) + (add-hook 'after-save-hook #'gloss--after-save-refresh-cache nil t) + marker)) ;;;###autoload (defun gloss-fetch-online (term) @@ -146,25 +220,93 @@ Returns a symbol naming the action taken: :show, :auto-save, :pick, (defun gloss-drill-export () "Tag every entry as :drill: for `org-drill'." (interactive) - (user-error "gloss-drill-export: not yet implemented")) + (gloss-drill-export-all)) ;;;###autoload (defun gloss-list-terms () - "Browse glossary terms via `completing-read'." + "Browse glossary terms via `completing-read' and show the chosen one." (interactive) - (user-error "gloss-list-terms: not yet implemented")) + (let ((terms (gloss-core-list))) + (unless terms + (user-error "gloss: glossary is empty")) + (let ((chosen (completing-read "Term: " terms nil t))) + (when chosen + (gloss--lookup-flow chosen))))) + +(defun gloss--count-drill-tagged () + "Return the number of top-level entries in `gloss-file' tagged :drill:. +Returns 0 if `gloss-file' does not exist." + (if (and gloss-file (file-exists-p gloss-file)) + (with-current-buffer (find-file-noselect gloss-file) + (unless (verify-visited-file-modtime (current-buffer)) + (revert-buffer t t t)) + (unless (derived-mode-p 'org-mode) + (let ((org-mode-hook nil)) (org-mode))) + (let ((count 0)) + (org-map-entries + (lambda () + (when (and (= 1 (org-current-level)) + (member "drill" (org-get-tags nil t))) + (setq count (1+ count))))) + count)) + 0)) + +(defun gloss--stats-text () + "Return a multi-line string summarizing glossary state." + (gloss-core--cache-ensure-or-init) + (let* ((terms (gloss-core-list)) + (total (length terms)) + (by-source (make-hash-table :test 'equal)) + (drill-count (gloss--count-drill-tagged)) + (file-size (when (and gloss-file (file-exists-p gloss-file)) + (file-attribute-size (file-attributes gloss-file)))) + (mtime gloss-core--cache-mtime)) + (dolist (term terms) + (let* ((entry (gloss-core-lookup term)) + (source (or (plist-get entry :source) 'unknown))) + (puthash source (1+ (or (gethash source by-source) 0)) by-source))) + (let (source-pairs) + (maphash (lambda (k v) (push (cons k v) source-pairs)) by-source) + (format "Glossary stats: + Total terms: %d + By source: %s + Drill-tagged: %d + File size: %s bytes + Cache mtime: %s +" + total + (if source-pairs + (mapconcat (lambda (pair) + (format "%s=%d" (car pair) (cdr pair))) + source-pairs ", ") + "(none)") + drill-count + (or file-size 0) + (if mtime + (format-time-string "%Y-%m-%d %H:%M:%S" mtime) + "never"))))) ;;;###autoload (defun gloss-stats () - "Summarize the glossary state." + "Show glossary statistics in a side buffer." (interactive) - (user-error "gloss-stats: not yet implemented")) + (let ((buf (get-buffer-create "*gloss-stats*"))) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (gloss--stats-text))) + (special-mode) + (goto-char (point-min))) + (display-buffer buf) + buf)) ;;;###autoload (defun gloss-reload () "Force reload of the glossary cache from disk." (interactive) - (user-error "gloss-reload: not yet implemented")) + (gloss-core--cache-reset) + (gloss-core--cache-ensure) + (message "gloss: cache reloaded")) ;;;###autoload (defun gloss-toggle-debug () diff --git a/tests/test-gloss--stats-text.el b/tests/test-gloss--stats-text.el index 32093b1..d91a4e6 100644 --- a/tests/test-gloss--stats-text.el +++ b/tests/test-gloss--stats-text.el @@ -30,8 +30,8 @@ (should (string-match-p "File size:" text)) (should (string-match-p "Cache mtime:" text))))) -(ert-deftest test-gloss-stats-text-missing-file-reports-zero-and-never () - "Error: missing glossary file reports zero terms and \"never\" mtime." +(ert-deftest test-gloss-stats-text-missing-file-reports-zero () + "Boundary: stats-text against a missing file creates it, reports zero terms." (gloss-test--with-missing-glossary (let ((text (gloss--stats-text))) (should (string-match-p "Total terms: +0" text))))) |
