dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

commit 2913411487c78af2bef6cc6a43e39cfb015070de
parent 577494d4e184b362aeadb4b9d937939014fa96d8
Author: Alex Balgavy <alexander.balgavy@spaceapplications.com>
Date:   Tue,  1 Sep 2026 17:53:11 +0200

emacs: update config

Diffstat:
Memacs/config.org | 16+++++++++++++---
Memacs/jokull-theme.el | 7++++---
Aemacs/lisp/hywiki-alias.el | 609+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 626 insertions(+), 6 deletions(-)

diff --git a/emacs/config.org b/emacs/config.org @@ -580,6 +580,10 @@ The "(interactive)" means that it can be called from a keybinding or from M-x. #+begin_src emacs-lisp (bind-key (kbd "C-x x n") #'clone-indirect-buffer) #+end_src +** Find function source code +#+begin_src emacs-lisp + (bind-key "C-h M-k" #'find-function-on-key) +#+end_src * Custom functions ** Make region readonly or writable #+begin_src emacs-lisp @@ -1194,6 +1198,9 @@ Hide some messages I don't need. ** hyperbole #+begin_src emacs-lisp (use-package hyperbole + :custom + (hyrolo-file-list za/hyrolo-file-list) + (hywiki-directory za/hywiki-directory) :config (defib za/ib-cdmidev () "CDMIDEV links" @@ -1206,9 +1213,12 @@ Hide some messages I don't need. (looking-at "CDMI\\(?:DEV\\|OPS\\)-[0-9]+"))) (let ((work-item (apply #'buffer-substring-no-properties (seq-take (match-data) 2)))) (ibut:label-set work-item) - (hact 'www-url (concat "https://csde.esa.int/jira/browse/" work-item)))))) - (hyperbole-mode 1) - (hkey-ace-window-setup) + (hact 'www-url (concat "https://csde.esa.int/jira/browse/" (upcase work-item)))))) + (hyperbole-mode 1) + (hkey-ace-window-setup) + (hywiki-mode :all) + (require 'hywiki-alias) + (zetta-hywiki-alias-mode 1)) #+end_src ** which-key Minor mode for Emacs that displays the key bindings following your currently entered incomplete command (a prefix) in a popup. diff --git a/emacs/jokull-theme.el b/emacs/jokull-theme.el @@ -154,7 +154,7 @@ `(undo-tree-visualizer-unmodified-face ((,class :foreground ,var))) `(undo-tree-visualizer-register-face ((,class :foreground ,type))) `(slime-repl-inputed-output-face ((,class (:foreground ,type)))) - `(trailing-whitespace ((,class :foreground nil :background ,warning))) + `(trailing-whitespace ((,class :foreground unspecified :background ,warning))) `(rainbow-delimiters-depth-1-face ((,class :foreground ,fg1))) `(rainbow-delimiters-depth-2-face ((,class :foreground ,type))) `(rainbow-delimiters-depth-3-face ((,class :foreground ,var))) @@ -211,7 +211,7 @@ `(helm-grep-file ((,class (:foreground ,fg1 :background ,bg1)))) `(helm-grep-finish ((,class (:foreground ,fg2 :background ,bg1)))) `(helm-grep-lineno ((,class (:foreground ,fg1 :background ,bg1)))) - `(helm-grep-match ((,class (:foreground nil :background nil :inherit helm-match)))) + `(helm-grep-match ((,class (:foreground undefined :background nil :inherit helm-match)))) `(helm-grep-running ((,class (:foreground ,func :background ,bg1)))) `(helm-moccur-buffer ((,class (:foreground ,func :background ,bg1)))) `(helm-source-go-package-godoc-description ((,class (:foreground ,str)))) @@ -248,7 +248,8 @@ `(jde-java-font-lock-constant-face ((t (:foreground ,const)))) `(jde-java-font-lock-modifier-face ((t (:foreground ,fg2)))) `(jde-jave-font-lock-protected-face ((t (:foreground ,keyword)))) - `(jde-java-font-lock-number-face ((t (:foreground ,var))))) + `(jde-java-font-lock-number-face ((t (:foreground ,var)))) + `(hywiki--word-face ((t (:foreground ,const :underline t))))) ;; emacs >= 26.1 (when (>= emacs-major-version 26) (custom-theme-set-faces diff --git a/emacs/lisp/hywiki-alias.el b/emacs/lisp/hywiki-alias.el @@ -0,0 +1,609 @@ +;;; hywiki-alias.el --- Derived case/space aliases for HyWikiWords -*- lexical-binding: t; -*- + +;; Highlight and activate case- and space-variants of existing HyWikiWords +;; without typing any aliases. A HyWikiWord like `DataModelTesting' is split at +;; its CamelCase boundaries (Data | Model | Testing); the mode then highlights +;; any case-insensitive occurrence with optional single spaces at those +;; boundaries -- "Data Model Testing", "data model testing", "DaTa moDel +;; TESTING" -- and the Action Key (M-RET) on such a phrase jumps to the +;; `DataModelTesting' page, exactly as on the real WikiWord. The alias set is +;; derived from your live HyWiki pages, so there is nothing to maintain. +;; +;; This is an EDITING-TIME convenience only: highlighting + Action-Key jump. +;; It does NOT feed HyWiki's data model, so backlinks, publishing, the +;; hywiki-graph, cross-file grep and completion do not see these aliases. See +;; hywiki-alias.README.md for the full list and the rationale. +;; +;; Implementation is a thin, reversible layer over HyWiki: two pieces of advice +;; plus its own overlay pass. Disabling the mode removes both and all overlays. +;; Off by default -- `M-x zetta-hywiki-alias-mode' to toggle. + +(require 'cl-lib) + +(declare-function hywiki-get-wikiword-list "hywiki") +(declare-function hywiki-active-in-current-buffer-p "hywiki") +(declare-function hywiki-word-at "hywiki") +(declare-function hywiki-maybe-highlight-references "hywiki") +(declare-function hywiki-add-referent "hywiki") +(declare-function hywiki-add-page "hywiki") +(declare-function hywiki-find-referent "hywiki") +(declare-function hywiki-word-strip-suffix "hywiki") +(declare-function hywiki-word-create-and-display "hywiki") +(declare-function hywiki-get-plural-wikiword "hywiki") +(declare-function hywiki-get-singular-wikiword "hywiki") +(defvar hywiki-allow-plurals-flag) +(defvar hywiki-word-face) +(defvar hywiki-directory) +(defvar hywiki-file-suffix) +(defvar zetta-hywiki-alias-mode) + +(defgroup zetta-hywiki-alias nil + "Derived case/space aliases for HyWikiWords." + :group 'hyperbole-hywiki) + +(defcustom zetta-hywiki-alias-min-length 1 + "Only HyWikiWords at least this many characters get a derived alias. +The default, 1, imposes no real length floor; raise it to suppress aliases +for very short page names, whose lowercase forms are the most prose-prone." + :type 'integer :group 'zetta-hywiki-alias) + +(defcustom zetta-hywiki-alias-min-segments 1 + "Minimum CamelCase segments a HyWikiWord needs to get a derived alias. +The default, 1, aliases every page including single-word ones like `Emacs', +so lowercase `emacs' is highlighted and activates. Set to 2 to skip +single-word pages, whose case-insensitive match tends to light up prose; use +`zetta-hywiki-alias-deny-list' to exclude specific offenders either way." + :type 'integer :group 'zetta-hywiki-alias) + +(defcustom zetta-hywiki-alias-deny-list nil + "HyWikiWords that should never get a derived alias (e.g. common phrases)." + :type '(repeat string) :group 'zetta-hywiki-alias) + +(defcustom zetta-hywiki-alias-derive-plurals t + "Non-nil means also alias the plural/singular inflections of each page. +When set, a `Lisp' page also highlights and activates `lisps', a `Class' page +`classes', and so on, using HyWiki's own inflection rules +\(`hywiki-get-plural-wikiword' / `hywiki-get-singular-wikiword'). Both the +derived CamelCase aliases and manual `Aliases' entries are inflected, in both +directions (the plural of a singular name and the singular of a plural one). + +This mirrors -- and defers to -- HyWiki's native `hywiki-allow-plurals-flag', +which HyWiki applies only to the capitalized WikiWord form (so it highlights +`Lisps' but never lowercase `lisps'); enabling this extends the same plurals +to the lowercase/spaced/hyphenated alias forms. Because the underlying +HyWiki functions return nil when `hywiki-allow-plurals-flag' is nil, turning +HyWiki's plurals off turns these off too. Set to nil to match an alias only +in the exact number the page name uses." + :type 'boolean :group 'zetta-hywiki-alias) + +(defcustom zetta-hywiki-alias-wikify-key "C-c W" + "Key globally bound to `zetta-hywiki-alias-wikify', or nil for no binding. +A `keymap-set'-style string such as \"C-c W\". Set it through Customize (which +moves the binding for you) or `setq' it before this module loads; the module +installs the binding once at load time. Set to nil to leave the command +reachable only via \\[execute-extended-command]." + :type '(choice (const :tag "No binding" nil) (string :tag "Key")) + :set (lambda (sym val) + (when (and (boundp sym) (symbol-value sym)) + (ignore-errors (keymap-global-unset (symbol-value sym) t))) + (set-default sym val) + (when val (keymap-global-set val #'zetta-hywiki-alias-wikify))) + :group 'zetta-hywiki-alias) + +(defvar zetta-hywiki-alias--index nil + "Hash mapping a downcased, space-stripped alias form to its WikiWord(s). +The value is the LIST of canonical WikiWords that share the alias, sorted for +a stable representative; more than one entry means the alias is ambiguous and +the choice between pages is made at activation time.") +(defvar zetta-hywiki-alias--regexp nil + "Cached alternation regexp matching every derived alias form.") +(defvar zetta-hywiki-alias--generation 0 + "Counter bumped whenever the alias set changes. +Part of the `post-command' refresh-guard key, so adding a HyWikiWord forces +the next command to re-scan even when buffer text and scroll are unchanged.") + +(defun zetta-hywiki-alias--segments (word) + "Split WORD at CamelCase boundaries into a list of segments. +Handles acronym runs, so \"HTMLParser\" -> (\"HTML\" \"Parser\")." + (let* ((case-fold-search nil) + (s (replace-regexp-in-string + "\\([[:upper:]]\\)\\([[:upper:]][[:lower:]]\\)" "\\1\0\\2" word)) + (s (replace-regexp-in-string + "\\([[:lower:][:digit:]]\\)\\([[:upper:]]\\)" "\\1\0\\2" s))) + (split-string s "\0" t))) + +(defun zetta-hywiki-alias--page-file (word) + "Return WORD's readable HyWiki page file, or nil." + (when (and (boundp 'hywiki-directory) hywiki-directory) + (let ((f (expand-file-name + (concat word (if (boundp 'hywiki-file-suffix) hywiki-file-suffix ".org")) + hywiki-directory))) + (and (file-readable-p f) f)))) + +(defun zetta-hywiki-alias--hywiki-page-file-p (file) + "Return non-nil if FILE is a HyWiki page file directly under `hywiki-directory'." + (and file (boundp 'hywiki-directory) hywiki-directory + (let ((f (expand-file-name file)) + (dir (file-name-as-directory (expand-file-name hywiki-directory))) + (suffix (if (boundp 'hywiki-file-suffix) hywiki-file-suffix ".org"))) + (and (string-suffix-p suffix f) + (equal (file-name-directory f) dir))))) + +(defun zetta-hywiki-alias--file-aliases (word) + "Return the manual alias strings declared in WORD's page `Aliases' section. +Reads WORD's page file and collects each entry beneath a heading whose title +is `Aliases' (any level, case-insensitive), up to the next heading. Leading +list bullets are stripped; blank lines and Org keyword/comment lines (`#...') +are ignored." + (let ((file (zetta-hywiki-alias--page-file word))) + (when file + (with-temp-buffer + (insert-file-contents file) + (goto-char (point-min)) + (let ((case-fold-search t) aliases) + (when (re-search-forward "^\\*+[ \t]+aliases[ \t]*$" nil t) + (forward-line 1) + (while (and (not (eobp)) (not (looking-at-p "^\\*+[ \t]"))) + (let ((line (string-trim (buffer-substring-no-properties + (line-beginning-position) + (line-end-position))))) + (setq line (replace-regexp-in-string + "\\`\\(?:[-+*]\\|[0-9]+[.)]\\)[ \t]+" "" line)) + (when (and (not (string-empty-p line)) + (not (string-prefix-p "#" line))) + (push line aliases))) + (forward-line 1))) + (nreverse aliases)))))) + +(defun zetta-hywiki-alias--add (index canon tokens) + "Add an alias built from TOKENS -> CANON to INDEX and return its regexp. +TOKENS is the ordered list of word pieces; in text they may be joined, or +separated by a single space/tab or hyphen. INDEX maps each alias key to the +LIST of canonical WikiWords that claim it, so when several pages share an +alias (e.g. two people both aliased `programmer') every page is kept and +offered at activation time instead of one silently clobbering the other. +CANON is appended when not already present. Returns nil for empty TOKENS." + (when tokens + (let* ((key (downcase (apply #'concat tokens))) + (existing (gethash key index))) + (unless (member canon existing) + (puthash key (append existing (list canon)) index))) + (mapconcat #'regexp-quote tokens "[ \t-]?"))) + +(defun zetta-hywiki-alias--number-variants (s) + "Return plural/singular inflections of S that differ from it, else nil. +Reuses HyWiki's own inflection rules so a lowercase or spaced alias +pluralizes exactly as HyWiki pluralizes the WikiWord itself -- e.g. a `Lisp' +page also lights up `lisps', and a `Class' page `classes'. Both directions +are produced (plural of a singular name, singular of a plural name), matching +HyWiki's bidirectional plural handling. + +HyWiki's singularizer strips a whole `-es' from sibilant endings, which is +right for `Boxes'->`Box' but wrong for the many plurals whose stem ends in a +silent `e' (`Houses'->`Hous', `Pages' left unchanged). So when S looks +plural we ALSO offer the naive strip-one-trailing-s singular, which recovers +`House'/`Page'/`Case'; any bogus extra (`hous') is inert since it never +occurs in prose. + +Honoured only when `zetta-hywiki-alias-derive-plurals' is non-nil; the HyWiki +functions additionally return their input unchanged unless +`hywiki-allow-plurals-flag' is set, so this quietly follows HyWiki's own +plural setting." + (when zetta-hywiki-alias-derive-plurals + (let (variants) + (cl-flet ((add (v) + (when (and (stringp v) (not (string-empty-p v)) + (not (equal v s)) (not (member v variants))) + (push v variants)))) + ;; HyWiki's own inflectors, in both directions. + (dolist (fn '(hywiki-get-plural-wikiword hywiki-get-singular-wikiword)) + (when (fboundp fn) (add (funcall fn s)))) + ;; Naive singular for plural-looking names, to cover the `-se' plurals + ;; HyWiki's `-es' rule mishandles. Skip `-ss' endings (`Class') and + ;; `emacs', and require a non-trivial stem. + (let ((low (downcase s))) + (when (and (> (length s) 3) + (string-suffix-p "s" low) + (not (string-suffix-p "ss" low)) + (not (equal low "emacs"))) + (add (substring s 0 -1))))) + (nreverse variants)))) + +(defun zetta-hywiki-alias--rebuild () + "Rebuild the alias index and matching regexp from existing HyWikiWords. +Includes both aliases derived from each WikiWord's CamelCase segments and any +manual aliases listed in a page's `Aliases' section." + (let ((index (make-hash-table :test 'equal)) + (patterns nil)) + (dolist (word (and (fboundp 'hywiki-get-wikiword-list) + (hywiki-get-wikiword-list))) + (when (stringp word) + ;; Derived aliases: case/space/hyphen variants of the CamelCase segments. + (let ((segs (zetta-hywiki-alias--segments word))) + (when (and (>= (length segs) zetta-hywiki-alias-min-segments) + (>= (length word) zetta-hywiki-alias-min-length) + (not (member word zetta-hywiki-alias-deny-list))) + (push (zetta-hywiki-alias--add index word segs) patterns) + ;; ...and the same variants for its plural/singular inflections. + (dolist (variant (zetta-hywiki-alias--number-variants word)) + (push (zetta-hywiki-alias--add + index word (zetta-hywiki-alias--segments variant)) + patterns)))) + ;; Manual aliases from the page's `Aliases' section (always honoured), + ;; each inflected the same way. + (dolist (alias (zetta-hywiki-alias--file-aliases word)) + (push (zetta-hywiki-alias--add index word (split-string alias "[ \t-]+" t)) + patterns) + (dolist (variant (zetta-hywiki-alias--number-variants alias)) + (push (zetta-hywiki-alias--add + index word (split-string variant "[ \t-]+" t)) + patterns))))) + ;; Collision-shared aliases push identical patterns; keep the regexp tidy. + (setq patterns (delete-dups (delq nil patterns))) + ;; Longer phrases first so a short alias cannot pre-empt a longer one. + (setq patterns (sort patterns (lambda (a b) (> (length a) (length b))))) + ;; Sort each key's candidate list so the representative (and the activation + ;; prompt's default) is stable rather than hash-iteration order. + (maphash (lambda (k v) (puthash k (sort v #'string<) index)) index) + (setq zetta-hywiki-alias--index index + zetta-hywiki-alias--regexp + (and patterns + (concat "\\b\\(?:" (mapconcat #'identity patterns "\\|") "\\)\\b"))))) + +(defun zetta-hywiki-alias--ensure () + "Build the index and regexp if they are not current." + (unless zetta-hywiki-alias--index (zetta-hywiki-alias--rebuild))) + +(defun zetta-hywiki-alias--invalidate (&rest _) + "Rebuild-on-demand the alias set and re-highlight all visible windows. +Advised onto the HyWikiWord-adding commands so a newly created word's derived +aliases appear immediately. Drop the cached index/regexp, bump the generation +counter (part of the `post-command' change-guard key, so a scroll-free, +edit-free buffer still re-scans on its next command), and refresh every visible +window now -- creating a WikiWord moves focus to the new page buffer, so the +buffer holding the alias occurrences is usually no longer the selected window." + (setq zetta-hywiki-alias--index nil + zetta-hywiki-alias--regexp nil) + (cl-incf zetta-hywiki-alias--generation) + (when (bound-and-true-p zetta-hywiki-alias-mode) + (zetta-hywiki-alias--refresh-windows))) + +(defun zetta-hywiki-alias--hywiki-face-at (pos) + "Return non-nil if a HyWiki highlight overlay already covers POS." + (seq-find (lambda (o) (eq (overlay-get o 'face) hywiki-word-face)) + (overlays-at pos))) + +(defun zetta-hywiki-alias--link-color-at (pos) + "Return a clickable link's colour at POS, or nil when POS is not a link. +Used to underline a WikiWord that is also a link in the link's own colour, so +it reads as both. Recognises `shr'/eww links and `button' buttons." + (cond + ((and (get-text-property pos 'shr-url) (facep 'shr-link)) + (face-attribute 'shr-link :foreground nil t)) + ((and (get-text-property pos 'button) (facep 'button)) + (face-attribute 'button :foreground nil t)))) + +(defun zetta-hywiki-alias--hyphen-bounded-p (mb me) + "Return non-nil if [MB, ME) is joined by a hyphen to another word. +This skips an alias that is only part of a larger hyphenated token -- e.g. +`emacs' inside `emacs-foobar' -- while still allowing a hyphen that lies +between the WikiWord's own segments, since that hyphen is consumed inside the +match rather than sitting at its edge." + (cl-flet ((wordish (c) (and c (eq ?w (char-syntax c))))) + (or (and (eq (char-after me) ?-) (wordish (char-after (1+ me)))) + (and (eq (char-before mb) ?-) (wordish (char-before (1- mb))))))) + +(defun zetta-hywiki-alias--highlight-region (start end) + "Highlight derived HyWikiWord aliases between START and END." + (zetta-hywiki-alias--ensure) + (when zetta-hywiki-alias--regexp + (remove-overlays start end 'zetta-hywiki-alias-p t) + (save-excursion + (goto-char start) + (let ((case-fold-search t)) + (while (re-search-forward zetta-hywiki-alias--regexp end t) + (let* ((mb (match-beginning 0)) + (me (match-end 0)) + (text (match-string-no-properties 0)) + (key (downcase (replace-regexp-in-string "[ \t-]+" "" text))) + (cands (gethash key zetta-hywiki-alias--index)) + (canon (car cands)) + (link-color (zetta-hywiki-alias--link-color-at mb)) + (hy-ov (zetta-hywiki-alias--hywiki-face-at mb)) + ;; Does a HyWiki overlay already span our whole match? If so it + ;; owns the exact WikiWord and we defer. If it covers only a + ;; sub-part -- e.g. `Emacs' inside a longer `Emacs Completion' + ;; whose joined form is the page `EmacsCompletion' -- we take + ;; over so the longest (composite) WikiWord wins as one unit. + (hy-covers-all (and hy-ov (>= (overlay-end hy-ov) me)))) + (when (and canon + ;; Not merely part of a larger hyphenated token: catch + ;; `emacs-completion' (EmacsCompletion) but not `emacs' + ;; inside `emacs-foobar'. + (not (zetta-hywiki-alias--hyphen-bounded-p mb me)) + ;; Defer only to a HyWiki overlay that already covers the + ;; whole match; on a link we still layer on for the cue. + ;; Elsewhere -- HyWiki idle (eww), a spot it skipped, or a + ;; composite it split -- we highlight the match ourselves. + (or link-color (not hy-covers-all))) + ;; Composite override: drop any HyWiki sub-part overlays inside our + ;; span so the composite shows and activates as a single WikiWord. + (unless hy-covers-all + (dolist (o (overlays-in mb me)) + (when (eq (overlay-get o 'face) hywiki-word-face) + (delete-overlay o)))) + (let ((ov (make-overlay mb me))) + (overlay-put ov 'zetta-hywiki-alias canon) + (overlay-put ov 'zetta-hywiki-alias-candidates cands) + (overlay-put ov 'zetta-hywiki-alias-p t) + ;; A DISTINCT (anonymous) face -- looks identical to + ;; `hywiki-word-face' but is not `eq' to it, so HyWiki's own + ;; per-command dehighlight (which clears overlays *by* that face + ;; value) does not sweep our alias overlays away. On a link, add + ;; the link's own colour as the underline and sit above HyWiki's + ;; overlay, so the word reads as both a WikiWord (orange text) + ;; and a clickable link (coloured underline). + (overlay-put ov 'face + (if link-color + (list :inherit hywiki-word-face :underline link-color) + (list :inherit hywiki-word-face))) + (when link-color (overlay-put ov 'priority 100)) + (overlay-put ov 'evaporate t) + (overlay-put ov 'help-echo + (cond + ((cdr cands) + (format "HyWiki alias -> %s (choose on activation)" + (mapconcat #'identity cands " | "))) + ((string= text canon) + (format "HyWikiWord: %s" canon)) + (t + (format "HyWiki alias -> %s" canon)))))))))))) + +(defun zetta-hywiki-alias--refresh-region (start end) + "Re-highlight derived aliases between START and END, expanded to whole lines." + (zetta-hywiki-alias--highlight-region + (save-excursion (goto-char start) (line-beginning-position)) + (save-excursion (goto-char (min end (point-max))) (line-end-position)))) + +(defvar-local zetta-hywiki-alias--last nil + "Cache key (tick window-start window-end) of the last visible-region refresh. +Skips redundant rescans so `post-command-hook' stays cheap and flicker-free.") + +(defun zetta-hywiki-alias--post-command () + "Refresh alias highlighting in the selected window's visible region. +Driven off `post-command-hook' so highlights appear promptly and survive +HyWiki's own per-command dehighlight passes. Only rescans when the buffer +was modified or the window scrolled since the last refresh." + (when (and (bound-and-true-p zetta-hywiki-alias-mode) + (fboundp 'hywiki-active-in-current-buffer-p) + (hywiki-active-in-current-buffer-p)) + (let* ((win (selected-window)) + (ws (window-start win)) + (we (window-end win t)) + (key (list zetta-hywiki-alias--generation + (buffer-chars-modified-tick) ws we))) + (unless (equal key zetta-hywiki-alias--last) + (setq zetta-hywiki-alias--last key) + (zetta-hywiki-alias--refresh-region ws we))))) + +(defun zetta-hywiki-alias--refresh-window (win) + "Re-highlight derived aliases in WIN's visible region. +Highlights WIN by its own bounds rather than via the selected window, so a +visible but unselected window -- e.g. the buffer you were editing after focus +moved to a freshly created page -- is refreshed too." + (with-current-buffer (window-buffer win) + (when (and (fboundp 'hywiki-active-in-current-buffer-p) + (hywiki-active-in-current-buffer-p)) + (let ((ws (window-start win)) + (we (window-end win t))) + ;; Record the guard key so the buffer's own next `post-command' pass + ;; skips a redundant (flicker-inducing) rescan when it regains focus. + (setq zetta-hywiki-alias--last + (list zetta-hywiki-alias--generation + (buffer-chars-modified-tick) ws we)) + (zetta-hywiki-alias--refresh-region ws we))))) + +(defun zetta-hywiki-alias--refresh-windows () + "Force an alias refresh in every visible window on every frame. +Used on mode enable and whenever the alias set changes." + (walk-windows #'zetta-hywiki-alias--refresh-window nil t)) + +(defun zetta-hywiki-alias--word-at-advice (orig &optional range-flag hash-sign-only-flag) + "Make `hywiki-word-at' recognise a derived alias at point. +When point is on an alias overlay, return its canonical WikiWord -- as a +\(WORD START END) list when RANGE-FLAG is set; otherwise defer to ORIG." + (let ((canon (and (bound-and-true-p zetta-hywiki-alias-mode) + (get-char-property (point) 'zetta-hywiki-alias)))) + (if canon + (if range-flag + (let ((ov (seq-find (lambda (o) (overlay-get o 'zetta-hywiki-alias)) + (overlays-at (point))))) + (list canon (and ov (overlay-start ov)) (and ov (overlay-end ov)))) + canon) + (funcall orig range-flag hash-sign-only-flag)))) + +(defun zetta-hywiki-alias--find-referent-advice (orig &optional wikiword prompt-flag) + "Disambiguate when the alias at point maps to several HyWiki pages. +`hywiki-find-referent' is the single navigation chokepoint every activation +path funnels through -- both the `hywiki-word' and `hywiki-existing-word' +implicit buttons and the Org `hy:' link -- and unlike `hywiki-word-at' it is +not called during highlighting, range detection or idle passes, so it is the +one safe place to prompt. + +When point sits on an alias overlay whose candidate list holds more than one +canonical WikiWord -- e.g. `programmer' declared by both CharlieHolland and +CharlieBaker -- and ORIG is about to visit the silently chosen representative +\(WIKIWORD), ask which page to open and route ORIG there instead. Every +other call -- a real WikiWord, a single-candidate alias, or an unrelated +navigation while point happens to rest on an alias -- passes straight +through, guarded by matching WIKIWORD against the overlay's representative." + (let* ((ov (and (bound-and-true-p zetta-hywiki-alias-mode) + (stringp wikiword) + (seq-find (lambda (o) (overlay-get o 'zetta-hywiki-alias-candidates)) + (overlays-at (point))))) + (cands (and ov (overlay-get ov 'zetta-hywiki-alias-candidates)))) + (if (and cands (cdr cands) + (fboundp 'hywiki-word-strip-suffix) + (equal (hywiki-word-strip-suffix wikiword) + (overlay-get ov 'zetta-hywiki-alias))) + (let ((choice (completing-read + (format "Alias \"%s\" -> HyWikiWord: " + (buffer-substring-no-properties + (overlay-start ov) (overlay-end ov))) + cands nil t nil nil (car cands)))) + (funcall orig choice prompt-flag)) + (funcall orig wikiword prompt-flag)))) + +(defun zetta-hywiki-alias--refresh-on-save () + "Rebuild aliases and re-highlight after saving a HyWiki page file. +On `after-save-hook' so edits to a page's `Aliases' section (or a new page +saved to disk) take effect at once, without a manual refresh." + (when (and (bound-and-true-p zetta-hywiki-alias-mode) + (zetta-hywiki-alias--hywiki-page-file-p buffer-file-name)) + (zetta-hywiki-alias--invalidate))) + +(defun zetta-hywiki-alias--rehighlight-hywiki () + "Re-run HyWiki's own WikiWord highlighting in every visible window. +Called on disable so toggling the mode off is a clean A/B against HyWiki's +native behaviour -- restoring its view even where we had replaced its overlays +\(e.g. composites in eww, which HyWiki never re-scans on its own)." + (when (fboundp 'hywiki-maybe-highlight-references) + (walk-windows + (lambda (win) + (with-current-buffer (window-buffer win) + (when (and (fboundp 'hywiki-active-in-current-buffer-p) + (hywiki-active-in-current-buffer-p)) + (ignore-errors (hywiki-maybe-highlight-references))))) + nil t))) + +;;; ------------------------------------------------------------------------ +;;; Creating a HyWikiWord from arbitrary text (the inverse of aliasing) +;;; ------------------------------------------------------------------------ + +(defun zetta-hywiki-alias-to-wikiword (text) + "Convert TEXT into a PascalCase HyWikiWord string, or nil if impossible. +This is the inverse of the aliasing this mode performs: it collapses any of +the manifestations the aliases would match back into one canonical WikiWord. + +Every run of non-alphabetic characters -- spaces, tabs, newlines, hyphens, +underscores, digits, punctuation -- separates words, and existing CamelCase +inside a run of letters is split too (reusing `zetta-hywiki-alias--segments', +the very splitter the aliases are built from). So `text embedding', +`text-embedding', `text_embedding', `TEXT EMBEDDING' and `textEmbedding' all +yield `TextEmbedding'. Each segment is then capitalized -- first letter +upper, rest lower -- so acronyms are title-cased (`HTML parser' -> +`HtmlParser'); that still round-trips because the aliases match +case-insensitively. + +Returns nil when TEXT has no letters, or yields only a single letter, since a +HyWikiWord must be an uppercase-initial, all-alphabetic word of at least two +characters. Digits cannot appear in a HyWikiWord, so they act purely as +separators (`gpt 4 turbo' -> `GptTurbo')." + (when (stringp text) + (let* ((words (split-string text "[^[:alpha:]]+" t)) + (segs (mapcan #'zetta-hywiki-alias--segments words)) + (word (mapconcat + (lambda (w) (concat (upcase (substring w 0 1)) + (downcase (substring w 1)))) + segs ""))) + (and (string-match-p "\\`[[:upper:]][[:alpha:]]+\\'" word) word)))) + +;;;###autoload +(defun zetta-hywiki-alias-wikify (beg end &optional stay) + "Create a HyWikiWord page from the region BEG..END, display it, keep the text. +Interactively, act on the active region; with none, use the symbol at point. +The text is converted to a PascalCase WikiWord via +`zetta-hywiki-alias-to-wikiword', its page is created and shown, but the prose +is left UNCHANGED: this mode immediately highlights it as an alias of the new +page, so `text embedding' lights up and activates in place -- without being +rewritten to the literal `TextEmbedding'. + +With a prefix arg STAY, create the page without leaving the current buffer. +Signals a `user-error' if the text cannot form a valid WikiWord or if HyWiki +is unavailable. Returns the WikiWord." + (interactive + (append (cond ((use-region-p) (list (region-beginning) (region-end))) + ((bounds-of-thing-at-point 'symbol) + (let ((b (bounds-of-thing-at-point 'symbol))) + (list (car b) (cdr b)))) + (t (user-error "No region or symbol at point to wikify"))) + (list current-prefix-arg))) + (unless (require 'hywiki nil t) + (user-error "Load GNU Hyperbole/HyWiki before using %s" + 'zetta-hywiki-alias-wikify)) + (let* ((text (buffer-substring-no-properties beg end)) + (word (zetta-hywiki-alias-to-wikiword text)) + (src (current-buffer))) + (unless word + (user-error "Cannot form a HyWikiWord from %S" text)) + ;; Create the page -- and by default open it -- but leave the prose alone. + (if stay + (hywiki-add-page word) + (hywiki-word-create-and-display word)) + ;; Make the new page's aliases live and light up the source phrase now. + ;; `hywiki-add-page' invalidates the alias set via advice, but + ;; `hywiki-word-create-and-display' can reach the page by another route, so + ;; invalidate explicitly -- rebuilding the index to include the new page -- + ;; then re-highlight the source region, which may no longer be in a visible + ;; window now that the page is displayed (so the generic refresh misses it). + (when (bound-and-true-p zetta-hywiki-alias-mode) + (zetta-hywiki-alias--invalidate) + (when (buffer-live-p src) + (with-current-buffer src + (when (and (fboundp 'hywiki-active-in-current-buffer-p) + (hywiki-active-in-current-buffer-p)) + (zetta-hywiki-alias--refresh-region beg end))))) + (when (called-interactively-p 'interactive) + (message "HyWikiWord %s: page created%s, source text left in place" + word (if stay "" " and opened"))) + word)) + +;;;###autoload +(define-minor-mode zetta-hywiki-alias-mode + "Global mode: highlight and activate case/space/hyphen variants of HyWikiWords. +Aliases are derived from your existing HyWiki pages, plus any listed in a page's +`Aliases' section; see hywiki-alias.README.md for details. + +This is the toggle between this module and HyWiki's native behaviour: turn it on +\(\\[zetta-hywiki-alias-mode]) for aliasing and composite handling, or off to +fall back to plain HyWiki -- disabling restores HyWiki's own highlighting in the +visible buffers." + :global t + :group 'zetta-hywiki-alias + (if zetta-hywiki-alias-mode + (if (not (require 'hywiki nil t)) + (progn + (setq zetta-hywiki-alias-mode nil) + (user-error "Load GNU Hyperbole/HyWiki before enabling %s" + 'zetta-hywiki-alias-mode)) + (zetta-hywiki-alias--rebuild) + (advice-add 'hywiki-word-at :around #'zetta-hywiki-alias--word-at-advice) + (advice-add 'hywiki-find-referent :around + #'zetta-hywiki-alias--find-referent-advice) + (advice-add 'hywiki-add-referent :after #'zetta-hywiki-alias--invalidate) + (advice-add 'hywiki-add-page :after #'zetta-hywiki-alias--invalidate) + (add-hook 'post-command-hook #'zetta-hywiki-alias--post-command) + (add-hook 'after-save-hook #'zetta-hywiki-alias--refresh-on-save) + (zetta-hywiki-alias--refresh-windows)) + (remove-hook 'post-command-hook #'zetta-hywiki-alias--post-command) + (remove-hook 'after-save-hook #'zetta-hywiki-alias--refresh-on-save) + (advice-remove 'hywiki-word-at #'zetta-hywiki-alias--word-at-advice) + (advice-remove 'hywiki-find-referent #'zetta-hywiki-alias--find-referent-advice) + (advice-remove 'hywiki-add-referent #'zetta-hywiki-alias--invalidate) + (advice-remove 'hywiki-add-page #'zetta-hywiki-alias--invalidate) + (dolist (buf (buffer-list)) + (with-current-buffer buf + (remove-overlays (point-min) (point-max) 'zetta-hywiki-alias-p t))) + (zetta-hywiki-alias--invalidate) + ;; Restore HyWiki's native highlighting in visible buffers so toggling off + ;; is a clean A/B against HyWiki's own behaviour. + (zetta-hywiki-alias--rehighlight-hywiki))) + +;; Enable automatically once HyWiki is available. This file loads at startup, +;; before Hyperbole's deferred load, so the mode turns on as soon as HyWiki +;; provides. Toggle it off any time with `M-x zetta-hywiki-alias-mode'. +;;(with-eval-after-load 'hywiki + ;;(zetta-hywiki-alias-mode 1)) + +(provide 'hywiki-alias) +;;; hywiki-alias.el ends here