diff --git a/ChangeLog b/ChangeLog index 29ceec38..eb765289 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,28 @@ +2026-02-27 Mats Lidell + +* test/hywiki-yki-tests.el (hywiki-test--insert-with-point): Remove. Reuse + similar function from hywiki-tests.el instead. + (hywiki-test--get-buffer-text-with-point-and-highlight): Change to a + more lifelike test that behaves the same in batch as interactively to + solve the issue with the failing test for now. Use + hywiki-tests--insert-by-char and remove not needed hywiki-mode calls. + +2026-02-26 Bob Weiner + +* hywiki.el (hywiki-word-strip-suffix): Fix to strip any leading or trailing # + and whitespace chars. + (hywiki-format-grep-to-reference): Don't add double quotes so when used + with completions, don't fail because the double quote is not included in + the matched prefix. + (hywiki-insert-link): Add double-quotes if link contains a space. + (hywiki-completion-exit-function): Add. + (hywiki-word-add-completion-at-point): Add call to above function in + completion exit hooks that support normal completion, company and corfu. + (hywiki-maybe-at-wikiword-beginning): Change when non-nil return value to + be the preceding char or if at bol, then 0. + (hywiki-word-add-completion-at-point, hywiki-completion-at-point): + Add support for Corfu with :exit-function. + 2026-02-25 Mats Lidell * .github/workflows/docs.yml: New workflow @@ -6,8 +31,50 @@ (doc-clean): Use make foreach for removing doc build artifacts. (Avoid bash brace expansion.) Remove README.md.html and README.toc.md. +2026-02-25 Bob Weiner + +* hbut.el (hbut:modify-syntax): Treat # as a symbol constituent to support + 'company-mode' completion in for 'hbut:syntax-table' and + 'help-mode-syntax-table'. + +2026-02-22 Bob Weiner + +* README.md: Add "The Emacs Hyperbole" great article by Mike Hostetler. + +* hywiki.el (hywiki-format-reference): Rename to 'hywiki-format-grep-to-reference'; + remove autoload. + (hywiki-format-reference-to-grep): Add. + (hywiki-completion-at-point): Rewrite and simplify. + (hywiki-word-highlight-in-buffers): Add call to + 'hywiki-word-add-completion-at-point' to ensure has completion hook independent + of whether the buffer's major mode changes. + (hywiki-word-remove-completion-at-point): Add and call in + 'hywiki-word-dehighlight-in-buffers'. + (hywiki-display-referent): Interactively, call 'hywiki-word-read' + rather than 'hwyiki-page-read-reference'. + (hywiki-read-page-reference): Rename to 'hywiki-page-read-reference' + and return a HyWiki completion alist of pages when completing to pages only. + (hywiki-format-reference-to-consult-grep): Add. + +2026-02-21 Bob Weiner + +* hywiki.el (hywiki-consult-page-and-line): Change output from a list to the + unparsed string returned as the selection by 'consult-grep'. + (hywiki-consult-page-and-line): Rename to + 'hywiki-consult-page-and-headline)'. + (hywiki-format-reference): Change arg 'page-and-line' to be + an unparsed string returned from the above function. + (hywiki-consult-page-and-headline, hywiki-read-page-reference, + hywiki-word-read, hywiki-word-read-new, hywiki-page-read, + hywiki-page-read-new): Add 'prompt' and 'initial' input arguments. + 2026-02-19 Bob Weiner +* hywiki.el (hywiki-insert-link): Add optional prefix arg to link to a + an existing wikiword only. + (hywiki-insert-reference): Remove and merge capability into + above function when called with a prefix arg. + * hui.el (hui:link-possible-types): Fix to include link-to-wikiword only if an existing referent is found. @@ -1344,7 +1411,7 @@ hyrolo.el (hyrolo-match-regexp) hhist.el (*hhist*): Use defvar for const variables that are set later than at their definition. - + 2025-08-10 Bob Weiner * test/hywiki-tests.el (hywiki-tests--with-face-test): Make default value be diff --git a/README.md b/README.md index 2124a699..e7385f4c 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ otherwise, skip to the next section. - [Quick Introduction](https://youtu.be/K1MNUctggwI) - [Top 10 ways Hyperbole amps up Emacs](https://emacsconf.org/2023/talks/hyperamp/) - + - [Introduction to Buttons](https://youtu.be/zoEht66N2PI) - [Linking Personal Info with Implicit Buttons](https://emacsconf.org/2022/talks/buttons/) @@ -44,19 +44,21 @@ otherwise, skip to the next section. - [HyRolo, fast contact/hierarchical record viewer](https://youtu.be/xdJGFdgKPFY) - [Using Koutline for stream of thought journaling](https://emacsconf.org/2023/talks/koutline/) - + - [Build a Zettelkasten with HyRolo](https://emacsconf.org/2022/talks/rolodex/) - [HyControl, fast Emacs frame and window manager](https://youtu.be/M3-aMh1ccJk) - [Writing test cases for GNU Hyperbole](https://emacsconf.org/2023/talks/test/) - + - [Find/Web Search](https://youtu.be/8lMlJed0-OM) - [Linking personal info with implicit buttons](https://youtu.be/TQ_fG7b1iHI) ## Articles + - [The Emacs Hyperbole](https://mike.hostetlerhome.com/emacs-hyperbole) + - [HyWiki: My Favorite Part of Hyperbole](https://kirankp.com/blog/gnu-hyperbole/) - [Hyperbole VisionQuest Part 1](https://github.com/termitereform/JunkPile/blob/master/HyperboleNotes.org) @@ -191,7 +193,7 @@ start moving further, faster. Once you have Emacs set up at your site, GNU Hyperbole may be installed by using the Emacs Package Manager. If you are not familiar with it, see the Packages section of the GNU Emacs Manual, -[Emacs Packages](https://www.gnu.org/software/emacs/manual/html_node/emacs/Packages.html). +[Emacs Packages](https://www.gnu.org/software/emacs/manual/html_node/emacs/Packages.html). If you have Hyperbole 5.10 or higher already installed and simply want to upgrade it, invoke the Emacs Package Manager with {M-x list-packages RET}, @@ -267,10 +269,10 @@ Hyperbole also includes the Hyperbole Manual, a full reference manual, not a simple introduction. It is included in the "man/" subdirectory of the Hyperbole package directory in four forms: -[hyperbole.info](man/hyperbole.info) - online Info browser version -[hyperbole.html](man/hyperbole.html) - web version -[hyperbole.pdf](man/hyperbole.pdf) - printable version -[hyperbole.texi](man/hyperbole.texi) - source version +[hyperbole.info](man/hyperbole.info) - online Info browser version +[hyperbole.html](man/hyperbole.html) - web version +[hyperbole.pdf](man/hyperbole.pdf) - printable version +[hyperbole.texi](man/hyperbole.texi) - source version The Hyperbole package installation places the Info version of this manual where needed and adds an entry for Hyperbole into the Info directory under @@ -323,7 +325,7 @@ Hyperbole consists of six parts: automatically recognized within text that perform actions, e.g. bug#24568 displays the bug status information for that bug number. - + These actions may be links or arbitrary Lisp expressions. So for example, you could create your own button type of Wikipedia searches that jumped to the named Wikipedia page @@ -403,7 +405,7 @@ to a file or executing a shell command. There are three categories of Hyperbole buttons: 1. *Explicit Buttons* - created by Hyperbole, accessible from within a single document; + created by Hyperbole, accessible from within a single document; 2. *Global Buttons* created by Hyperbole, accessible anywhere within a user's @@ -457,7 +459,7 @@ Some of Hyperbole's most important features include: Typical Hyperbole applications include: - - *Personal Information Management* + - *Personal Information Management* Overlapping link paths provide a variety of views into an information space. A single key press activates buttons regardless of their types, making navigation easy. @@ -465,7 +467,7 @@ Typical Hyperbole applications include: A search facility locates buttons in context and permits quick selection. - - *Documentation Browsing* + - *Documentation Browsing* Embedding cross-references in a favorite documentation format. Addition of a point-and-click interface to existing documentation. @@ -474,18 +476,18 @@ Typical Hyperbole applications include: of an identifier from its use within code or its reference within documentation. - - *Brainstorming* + - *Brainstorming* Capture of ideas and then quick reorganization with the Hyperbole Koutliner. Link to related ideas, eliminating the need to copy and paste information into a single place. - - *Help/Training Systems* + - *Help/Training Systems* Creation of tutorials with embedded buttons that show students how things work while explaining the concepts, e.g. an introduction to UNIX commands. This technique can be much more effective than descriptions alone. - - *Archive Managers* + - *Archive Managers* Supplementation of programs that manage archives from incoming information stream, having them add topic-based buttons that link to the archive holdings. Users can then search and create @@ -547,19 +549,19 @@ default context-sensitive Hyperbole key bindings (Smart Keys). well-thought out, poised engineering. It may be from the 90s, but it feels like a breath of fresh air to me. - -- de_sonnaz on reddit + -- de_sonnaz on reddit \*\*\* MAN I love Hyperbole!!! Wow! \*\*\* - -- Ken Olstad + -- Ken Olstad Cheyenne Software, Inc. ------- I *love* koutlines. - -- Bob Glickstein + -- Bob Glickstein Z-Code Software Corporation ------- @@ -580,7 +582,7 @@ default context-sensitive Hyperbole key bindings (Smart Keys). but due to the ease of installation and the quality of the documentation, digging into it is actually fun. - -- Aditya Siram + -- Aditya Siram ------- @@ -616,7 +618,7 @@ default context-sensitive Hyperbole key bindings (Smart Keys). useful set of power tools. If Emacs is your preferred productivity environment, it's definitely worth getting familiar with it. - -- Chris Nuzum + -- Chris Nuzum Co-founder, Traction Softwarea, Inc. ------- @@ -633,7 +635,7 @@ default context-sensitive Hyperbole key bindings (Smart Keys). perfect, but adequate), so I can put any aspect of development on our internal web for others to see. - -- Farzin Guilak + -- Farzin Guilak Protocol Systems, Inc., Engineer ------- @@ -656,7 +658,7 @@ default context-sensitive Hyperbole key bindings (Smart Keys). 4. The Hyperbole Koutliner, which I find a very useful tool. I've implemented Emacspeak extensions to support it. - -- TV Raman + -- TV Raman Google Inc. ------- @@ -679,7 +681,7 @@ default context-sensitive Hyperbole key bindings (Smart Keys). that do various little things, and I index saved mail messages by putting explicit link-to-mail buttons in an outline file. - -- Ken Olstad + -- Ken Olstad Cheyenne Software, Inc. ------- @@ -711,12 +713,12 @@ default context-sensitive Hyperbole key bindings (Smart Keys). * Organizing and viewing online documentation: using Hyperbole along with Hyper-man and Info makes it truly easy to look up online documentation. - + * Other desktop organization tasks: including links to various mail folders, saved newsgroup conversation threads, online note-taker, emacs-command invocations, etc. - -- Dadong Wan + -- Dadong Wan University of Hawaii ------- @@ -729,15 +731,15 @@ default context-sensitive Hyperbole key bindings (Smart Keys). in it is a very powerful capability. Using ange-ftp mode, one can make file references "across the world" as easily as normal file references. - -- Mark Eichin + -- Mark Eichin Cygnus Support ------- I just wanted to say how much I enjoy using the Hyperbole Koutliner. It is a great way to quickly construct very readable technical documents - that I can pass around to others. Thanks for the great work. + that I can pass around to others. Thanks for the great work. - -- Jeff Fried + -- Jeff Fried Informix ------- @@ -745,7 +747,7 @@ default context-sensitive Hyperbole key bindings (Smart Keys). The Hyperbole system provides a nice interface to exploring corners of Unix that I didn't know existed before. - -- Craig Smith + -- Craig Smith ## Why was Hyperbole developed? diff --git a/hbut.el b/hbut.el index 850fe36b..006ecb76 100644 --- a/hbut.el +++ b/hbut.el @@ -3,7 +3,7 @@ ;; Author: Bob Weiner ;; ;; Orig-Date: 18-Sep-91 at 02:57:09 -;; Last-Mod: 18-Feb-26 at 23:49:33 by Bob Weiner +;; Last-Mod: 25-Feb-26 at 22:31:30 by Bob Weiner ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -1638,6 +1638,9 @@ to include {} only. For use with implicit button activations." ;; Treat angle brackets and braces as opening and closing delimiters ;; for ease of matching. (mapc (lambda (syntax-table) + ;; Treat # as a symbol constituent to support company-mode completion. + (modify-syntax-entry ?# "_" syntax-table) + ;; Treat angle brackets as opening and closing delimiters for ease of matching. (modify-syntax-entry ?\< "(>" syntax-table) (modify-syntax-entry ?\> ")<" syntax-table) ;; Treat braces as opening and closing delimiters for ease of matching. diff --git a/hsys-org.el b/hsys-org.el index 43b99a2b..9ca6ef05 100644 --- a/hsys-org.el +++ b/hsys-org.el @@ -3,7 +3,7 @@ ;; Author: Bob Weiner ;; ;; Orig-Date: 2-Jul-16 at 14:54:14 -;; Last-Mod: 14-Feb-26 at 23:40:17 by Bob Weiner +;; Last-Mod: 22-Feb-26 at 23:14:50 by Bob Weiner ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -314,6 +314,40 @@ Return t if Org is reloaded, else nil." (add-to-list 'auto-mode-alist '("\\.org\\'" . org-mode)) t))))) +;;; Derived from `org-get-heading' in "org.el" +;;;###autoload +(defun hsys-org-format-heading (heading &optional no-tags no-todo no-priority no-comment) + "Return HEADING, without the leading asterisks. +When NO-TAGS is non-nil, don't include tags. +When NO-TODO is non-nil, don't include TODO keywords. +When NO-PRIORITY is non-nil, don't include priority cookie. +When NO-COMMENT is non-nil, don't include COMMENT string." + (when (stringp heading) + (let ((case-fold-search nil) + (org-complex-heading-regexp + "^\\(\\*+\\)\\(?: +\\(DONE\\|TODO\\)\\)?\\(?: +\\(\\[#.\\]\\)\\)?\\(?: +\\(.*?\\)\\)??\\(?:[ ]+\\(:[[:alnum:]_@#%:]+:\\)\\)?[ ]*$")) + (when (string-match org-complex-heading-regexp heading) + ;; When using `org-fold-core--optimise-for-huge-buffers', + ;; returned text will be invisible. Clear it up. + (save-match-data + (org-fold-core-remove-optimisation (match-beginning 0) (match-end 0))) + (let ((todo (and (not no-todo) (match-string 2 heading))) + (priority (and (not no-priority) (match-string 3 heading))) + (headline (pcase (match-string 4 heading) + (`nil "") + ((and (guard no-comment) h) + (replace-regexp-in-string + (eval-when-compile + (format "\\`%s[ \t]+" org-comment-string)) + "" h)) + (h h))) + (tags (and (not no-tags) (match-string 5)))) + ;; Restore cleared optimization. + (org-fold-core-update-optimisation (match-beginning 0) (match-end 0)) + (mapconcat #'identity + (delq nil (list todo priority headline tags)) + " ")))))) + (defun hsys-org-get-libraries-to-reload () "Return all org libraries that need to be reloaded to avoid mixed versions." (interactive) diff --git a/hui-mini.el b/hui-mini.el index 8c307dc2..fcc8fd7c 100644 --- a/hui-mini.el +++ b/hui-mini.el @@ -3,7 +3,7 @@ ;; Author: Bob Weiner ;; ;; Orig-Date: 15-Oct-91 at 20:13:17 -;; Last-Mod: 19-Feb-26 at 00:16:53 by Bob Weiner +;; Last-Mod: 19-Feb-26 at 22:59:55 by Bob Weiner ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -1070,7 +1070,8 @@ support underlined faces as well." '("Info" (id-info "(hyperbole)HyWiki") "Display Hyperbole manual section on HyWiki.") '("Link" hywiki-insert-link - "Prompt for and insert at point a HyWikiWord#section reference.") + "Prompt for and insert at point a HyWiki page#section reference. +With a prefix arg, insert a HyWikiWord instead.") '("ModeSet/" (menu . cust-hywiki-mode) "Set hywiki-mode state to determine where HyWikiWord references are recognized.") '("Org-M-RET/" (menu . cust-org) diff --git a/hywiki.el b/hywiki.el index b48e1caf..cef81098 100644 --- a/hywiki.el +++ b/hywiki.el @@ -3,7 +3,7 @@ ;; Author: Bob Weiner ;; ;; Orig-Date: 21-Apr-24 at 22:41:13 -;; Last-Mod: 19-Feb-26 at 19:29:13 by Bob Weiner +;; Last-Mod: 27-Feb-26 at 00:25:56 by Bob Weiner ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -407,13 +407,13 @@ where PATH is the un-resolvable reference." (defun hywiki--export-preparation-function (_project-plist) "Setup export hook functions." - (message "Hywiki export is in preparation.") + (message "Hywiki export being prepared...") (add-hook 'org-export-before-parsing-functions #'hywiki-org-export-function)) (defun hywiki--export-completion-function (_project-plist) "Remove export hook function." (remove-hook 'org-export-before-parsing-functions #'hywiki-org-export-function) - (message "Hywiki export is completed.")) + (message "Hywiki export completed.")) (defvar hywiki-org-publish-project-alist nil "HyWiki-specific export properties added to `org-publish-project-alist'.") @@ -581,7 +581,7 @@ including deletion commands and those in `hywiki-non-character-commands'." (hywiki-maybe-dehighlight-reference))) (if (and (symbolp this-command) - (string-match-p "^-?\\(insert\\|undo\\)\\(-\\|$\\)\\|eval-last-sexp\\|eval-expression\\|read--expression-try-read" (symbol-name this-command))) + (string-match-p "\\(^\\|-?\\)\\(insert\\|undo\\)\\(-\\|$\\)\\|eval-last-sexp\\|eval-expression\\|read--expression-try-read" (symbol-name this-command))) ;; prior to an insertion command (progn (setq hywiki--start (point) @@ -677,7 +677,7 @@ deletion commands and those in `hywiki-non-character-commands'." (hywiki-ignore-command-hooks-p)) (setq hywiki--range nil) (cond ((and (symbolp this-command) - (string-match-p "^-?\\(insert\\|undo\\)\\(-\\|$\\)\\|eval-last-sexp\\|eval-expression\\|read--expression-try-read" + (string-match-p "\\(^\\|-?\\)\\(insert\\|undo\\)\\(-\\|$\\)\\|eval-last-sexp\\|eval-expression\\|read--expression-try-read" (symbol-name this-command))) (setq hywiki--end (point)) (when (and hywiki--start (not (eq hywiki--start hywiki--end))) @@ -948,7 +948,7 @@ Return the WIKIWORD's referent if successfully found or nil otherwise. For further details, see documentation for `hywiki-find-referent'. After successfully finding a referent, run `hywiki-display-referent-hook'." - (interactive (list (hywiki-read-page-reference))) + (interactive (list (hywiki-word-read))) (let ((in-page-flag (null wikiword)) (in-hywiki-directory-flag (hywiki-in-page-p))) (if (or (stringp wikiword) in-hywiki-directory-flag) @@ -1484,50 +1484,43 @@ exists." prompt-flag))) (defun hywiki-completion-at-point () - "Complete HyWiki references, either the HyWikiWord or the #section." - (let ((ref-start-end (and (hywiki-active-in-current-buffer-p) - (not (hywiki-non-hook-context-p)) - (hywiki-word-at t t)))) - (when ref-start-end - (let* ((case-fold-search nil) - (opoint (point)) - (ref (nth 0 ref-start-end)) - (start (nth 1 ref-start-end)) - (end (nth 2 ref-start-end)) - ;; Extract the WikiWord before the '#' - (word (hywiki-word-from-reference ref))) - (save-excursion - ;; CASE 1. Look for the '#' delimiter on the current line - (if (re-search-backward "#" start t) - (let ((hash-pos (point)) - (page (expand-file-name (concat word ".org") hywiki-directory))) - ;; 2. Validate the WikiWord and page existence - (when (and (not (string-empty-p word)) - (file-readable-p page)) - (let* ((headings (hywiki-get-page-headings page)) - ;; Build the table with metadata manually - (table (lambda (str pred action) - (if (eq action 'metadata) - '(metadata (category . hywiki-heading)) - (complete-with-action action headings str pred))))) - (list (1+ hash-pos) - opoint - table - :exclusive 'no)))) - - ;; CASE 2: Standard WikiWord completion (no '#' found) - (let ((wikiword-list (hywiki-get-wikiword-list))) - ;; 2. Validate the WikiWord and page existence - (when (and start end wikiword-list (not (string-empty-p word))) - (let ((table - ;; Build the table with metadata manually - (lambda (str pred action) - (if (eq action 'metadata) - '(metadata (category . hywiki-page)) - (complete-with-action action wikiword-list str pred))))) - (list start end - table - :exclusive 'no)))))))))) + "Complete a HyWiki reference at point. +Each candidate is an alist with keys: file, line, text, and display." + (let* ((ref-start-end (and (hywiki-active-in-current-buffer-p) + (not (hywiki-non-hook-context-p)) + (hywiki-word-at t t))) + (ref (nth 0 ref-start-end)) + (partial-page-name (hywiki-get-singular-wikiword ref)) + (start (nth 1 ref-start-end)) + (end (nth 2 ref-start-end))) + (when start + (let* ((default-directory hywiki-directory) + (cmd (format "grep -nEH -r '^[ \t]*\\*+ +' ./*%s*%s" + partial-page-name + hywiki-file-suffix)) + (output (shell-command-to-string cmd)) + (lines (split-string output "\n" t)) + (candidate-alist + (mapcar #'list + (delq nil + (nconc (hywiki-get-page-list) + (mapcar #'hywiki-format-grep-to-reference lines)))))) + (when candidate-alist + (list start end candidate-alist + :exclusive 'no + ;; For company, allow any non-delim chars in prefix + ;; :company-prefix-length t + ;; :company-prefix-dirty t + ;; Returning the prefix as (string . t) tells Company: + ;; 'This is the prefix, and yes, it is currently valid (dirty).' + ;; :company-prefix-snapshot (cons ref t) + + ;; This prevents the minibuffer/Corfu/Company from + ;; re-parsing the # as a 'function quote' trigger. + :company-kind (lambda (_) 'keyword) + :annotation-function (lambda (_) " [HyWiki]") + ;; Corfu uses this + :exit-function (lambda (&rest _) (hywiki-completion-exit-function)))))))) (defun hywiki-create-referent-and-display (wikiword &optional prompt-flag) "Display the HyWiki referent for WIKIWORD if not in an ert test; return it. @@ -1647,17 +1640,15 @@ nil, else return \\='(page . \"\")." (and (or at-tag-flag (hsys-org-at-tags-p)) (or (hywiki-in-page-p) (string-prefix-p "*HyWiki Tags*" (buffer-name))))) -(defun hywiki-consult-page-and-line () - "Return a list of the file and line selected by consult or nil. -Use `hywiki-insert-reference' with the result of this function to insert a -double-quoted HyWikiWord reference at point." +(defun hywiki-consult-page-and-headline (&optional prompt initial) + "Return a string of HyWiki file and headline selected by `consult-grep' or nil." (interactive) - (let* ((dir (expand-file-name hywiki-directory)) - (manual-builder + (let* ((default-directory (expand-file-name hywiki-directory)) + (dir "./") + (builder (lambda (input) - (let* (;; Define the regex inside the builder so it's always in scope - (headline-pattern (concat "^\\* .*" input)) - ;; Ensure all arguments are evaluated as strings + (let* (;; Define the regexp inside the builder so it is always in scope + (headline-regexp (concat "^\\* .*" input)) (args (list "rg" "--null" "--line-buffered" @@ -1666,27 +1657,21 @@ double-quoted HyWikiWord reference at point." "--line-number" "--smart-case" "-g" "*.org" - "-e" headline-pattern + "-e" headline-regexp dir))) (cons args dir)))) (selected (consult--read - (consult--async-command manual-builder) - :prompt "HyWiki Headline: " + (consult--async-command builder) + :initial initial + :prompt (or prompt "HyWiki Headline: ") :require-match t :lookup #'consult--lookup-member :category 'consult-grep))) - - (when (stringp selected) - (if (string-match "\\`\\([^\0]+\\)\0\\([0-9]+\\):\\(.*\\)" selected) - (let ((file (match-string 1 selected)) - (line (match-string 3 selected))) - (list file line)) - (message "(hwiki-consult-file-and-line): Parse error on: %s" selected) - nil)))) + selected)) ;;;###autoload (defun hywiki-consult-grep (&optional regexp max-matches path-list prompt) - "Interactively search with a consult package grep command. + "Interactively search HyWiki pages with a consult package grep command. Search for optional REGEXP up to MAX-MATCHES in PATH-LIST or `hywiki-directory'. Use ripgrep (rg) if found, otherwise, plain grep. Initialize search with @@ -1796,7 +1781,10 @@ simplifies to: (hywiki--referent-reference-to-org-link reference referent description)))))) (defun hywiki-maybe-at-wikiword-beginning () - "Return non-nil if previous character is one preceding a HyWikiWord. + "Return non-nil if at bol or previous character is one preceding a HyWikiWord. +When non-nil, the value returned is 0 for bol and the preceding character, +otherwise. + Do not test whether or not a page exists for the HyWikiWord. Use `hywiki-get-referent' to determine whether a HyWiki page exists." ;; Ignore wikiwords preceded by any non-whitespace character, except @@ -1804,7 +1792,7 @@ Use `hywiki-get-referent' to determine whether a HyWiki page exists." (when (or (bolp) (string-match (regexp-quote (char-to-string (char-before))) "\[\(\{\<\"'`\t\n\r\f ")) - t)) + (or (char-before) 0))) (defun hywiki-directory-edit () "Edit HyWiki pages in current `hywiki-directory'. @@ -2077,62 +2065,98 @@ This includes the delimiters: (), {}, <>, [] and \"\" (double quotes)." result (list nil nil)))))) -(defun hywiki-read-page-reference () +(defun hywiki-page-read-reference (&optional prompt initial) "With consult package loaded, read a \"file^@line\" string, else a page name." (interactive) (if (featurep 'consult) - (hywiki-format-reference (hywiki-consult-page-and-line)) - ;; Without consult, can only complete to a HyWiki page - ;; without a section - (hywiki-page-read "Link to HyWiki page: "))) + (hywiki-format-grep-to-reference (hywiki-consult-page-and-headline + prompt + (hywiki-format-reference-to-consult-grep + initial))) + ;; Without consult, can only complete to a HyWiki page without a section. + ;; Make a completion table of page names. + (mapcar #'list (hywiki-get-page-list)))) ;;;###autoload -(defun hywiki-insert-link () - "Insert at point a link to a HyWiki page#section." - (interactive "*") - (let ((ref (hywiki-read-page-reference))) +(defun hywiki-insert-link (&optional arg) + "Insert at point a HyWiki page#section reference read from the minibuffer. +With optional prefix ARG non-nil, insert a HyWikiWord instead. +Add double quotes if the section contains any whitespace after trimming." + (interactive "*P") + (let ((ref (if arg (hywiki-word-read) (hywiki-page-read-reference)))) (when ref + (when (string-match-p "\\s-" ref) + (setq ref (concat "\"" ref ""\"))) (insert ref) (skip-chars-backward "\"") (goto-char (1- (point))) (hywiki-maybe-highlight-reference)))) -;;;###autoload -(defun hywiki-format-reference (page-and-line) - "Return a HyWikiWord#section reference from PAGE-AND-LINE. -Call `hywiki-consult-page-and-line' to generate PAGE-AND-LINE. +(defun hywiki-format-grep-to-reference (page-and-headline) + "Return a HyWikiWord#section reference from PAGE-AND-HEADLINE. Add double quotes if the section contains any whitespace after trimming. -Return t if PAGE-AND-LINE is a valid list, else nil. If the page name +Return t if PAGE-AND-HEADLINE is a valid string, else nil. If the page name therein is invalid, trigger an error." - (when (and page-and-line (listp page-and-line)) - (cl-destructuring-bind (page line) - page-and-line - (setq page (file-name-base page)) - (unless (and (string-match-p hywiki-word-regexp page) - (hywiki-page-exists-p page)) - (error "(hywiki-format-reference): Invalid HyWiki page name - \"%s\"" - page)) - ;; Drop '* ' prefix - (setq line (string-trim line "[ \t\n\r]*\\**[ \t\n\r]+")) - (format (if (string-match-p "\\s-" line) - "\"%s#%s\"" - "%s#%s") - page - line)))) - -(defun hywiki-insert-reference (page-and-line) - "Insert a HyWiki page#section reference from PAGE-AND-LINE. -Add double quotes if the section contains any whitespace after trimming. + (when (and page-and-headline (stringp page-and-headline)) + (if (string-match "\\`\\([^\0]+\\)[\0:]\\([0-9]+\\):\\(.*\\)" + page-and-headline) + (let ((page (file-name-base (match-string 1 page-and-headline))) + (line (match-string 3 page-and-headline))) + (setq line (string-trim line)) + ;; Drop '* ' prefix + (setq line (hsys-org-format-heading line t t t t)) + (format "%s#%s" page line)) + (message "(hwiki-format-grep-to-reference): Parse error on: %s" + page-and-headline) + nil))) + +(defun hywiki-format-reference-to-consult-grep (page-and-section) + "From PAGE-AND-SECTION ref, return '|^[ \t]*\\*+.*sect|page.*\.org' grep pat. -Return t if PAGE-AND-LINE is a valid list, else nil. If the page name +Return t if PAGE-AND-SECTION is a valid string, else nil. If the page name therein is invalid, trigger an error." - (let ((ref (hywiki-format-reference page-and-line))) - (when ref - (insert ref) - (skip-chars-backward "\"") - (goto-char (1- (point))) - t))) + (when (and page-and-section (stringp page-and-section)) + (if (string-match hywiki-word-with-optional-suffix-exact-regexp + page-and-section) + (let ((page (match-string 1 page-and-section)) + (line (match-string 2 page-and-section))) + (setq line (if line + ;; Remove the # prefix in line + (substring line 1) + "")) + ;; Add '* ' prefix + (format "|[ \t]*\\\\*+.*%s|%s" + (regexp-quote line) + (regexp-quote page) + ;; (regexp-quote hywiki-file-suffix) + )) + (message "(hwiki-format-reference-to-consult-grep): Parse error on: %s" + page-and-section) + nil))) + +(defun hywiki-format-reference-to-grep (page-and-section) + "From PAGE-AND-SECTION ref, return '|^[ \t]*\\*+.*sect|page.*\.org' grep pat. + +Return t if PAGE-AND-SECTION is a valid string, else nil. If the page name +therein is invalid, trigger an error." + (when (and page-and-section (stringp page-and-section)) + (if (string-match hywiki-word-with-optional-suffix-exact-regexp + page-and-section) + (let ((page (match-string 1 page-and-section)) + (line (match-string 2 page-and-section))) + (setq line (if line + ;; Remove the # prefix in line + (substring line 1) + "")) + ;; Add '* ' prefix + (format "[ \t]*\\\\*+.*%s" + (regexp-quote page) + (regexp-quote hywiki-file-suffix) + (regexp-quote line))) + (message "(hwiki-format-reference-to-grep): Parse error on: %s" + page-and-section) + nil))) (defun hywiki-maybe-dehighlight-balanced-pairs () "Before or after a balanced delimiter, dehighlight HyWikiWords within. @@ -3297,6 +3321,7 @@ variables." "Return PAGE-NAME with any optional #section:Lnum:Cnum stripped off. If an empty string or not a string, return nil." (when (and (stringp page-name) (not (string-empty-p page-name))) + (setq page-name (string-trim page-name "[# \t\n\r]+" "[# \t\n\r]+")) (if (and (string-match hywiki-word-with-optional-suffix-exact-regexp page-name) (or (match-beginning 2) (match-beginning 4))) ;; Remove any #section:Lnum:Cnum suffix in PAGE-NAME. @@ -3834,47 +3859,47 @@ these are handled by the Org mode link handler." (string-match "#[^][#()<>{}\"\n\r\f]+\\'" word) t))))) -(defun hywiki-word-read (&optional prompt) +(defun hywiki-word-read (&optional prompt initial) "Prompt with completion for and return an existing HyWikiWord. If point is on one, press RET immediately to use that one." (let ((completion-ignore-case t)) - (completing-read (if (stringp prompt) prompt "HyWikiWord: ") + (completing-read (if (stringp prompt) prompt "HyWiki Word: ") (hywiki-get-referent-hasht) - nil t nil nil (hywiki-word-at-point)))) + nil t initial nil (hywiki-word-at-point)))) -(defun hywiki-word-read-new (&optional prompt) +(defun hywiki-word-read-new (&optional prompt initial) "Prompt with completion for and return an existing or new HyWikiWord. If point is on one, press RET immediately to use that one." (let ((completion-ignore-case t)) - (completing-read (if (stringp prompt) prompt "HyWikiWord: ") + (completing-read (if (stringp prompt) prompt "HyWiki Word: ") (hywiki-get-referent-hasht) - nil nil nil nil (hywiki-word-at-point)))) + nil nil initial nil (hywiki-word-at-point)))) (defun hywiki-page-exists-p (word) "Return HyWiki WORD iff it is an existing page reference." (when (eq (car (hywiki-get-referent word)) 'page) word)) -(defun hywiki-page-read (&optional prompt) +(defun hywiki-page-read (&optional prompt initial) "Prompt with completion for and return an existing HyWiki page name. If point is on one, press RET immediately to use that one." (let* ((completion-ignore-case t) - (wikiword (hywiki-word-at-point)) + (wikiword (or initial (hywiki-word-at-point))) (page (hywiki-page-exists-p wikiword))) - (completing-read (if (stringp prompt) prompt "HyWiki page: ") + (completing-read (if (stringp prompt) prompt "HyWiki Page: ") (hywiki-get-page-list) - nil t nil nil (when page wikiword)))) + nil t initial nil (when page wikiword)))) -(defun hywiki-page-read-new (&optional prompt) +(defun hywiki-page-read-new (&optional prompt initial) "Prompt with completion for and return an existing/new HyWiki page name. If point is on one, press RET immediately to use that one." (let ((completion-ignore-case t) page) (while (null page) (setq page (completing-read - (if (stringp prompt) prompt "HyWiki page: ") + (if (stringp prompt) prompt "HyWiki Page: ") (hywiki-get-page-list) - nil nil nil nil (hywiki-word-at-point))) + nil nil initial nil (hywiki-word-at-point))) ;; Prevent selection of non-page HyWikiWords (unless (memq (car (hywiki-get-referent page)) '(page nil)) (setq page nil))) @@ -3934,6 +3959,7 @@ occurs with one of these hooks, the problematic hook is removed." "Auto-highlight HyWikiWords in BUFFERS." (dolist (buf buffers) (with-current-buffer buf + (hywiki-word-add-completion-at-point) (add-hook 'pre-command-hook 'hywiki-word-store-around-point -60 :local) (add-hook 'post-self-insert-hook 'hywiki-word-highlight-post-self-insert -60 :local) (add-hook 'post-command-hook 'hywiki-word-highlight-post-command -60 :local) @@ -3945,12 +3971,32 @@ occurs with one of these hooks, the problematic hook is removed." (hywiki-get-referent-hasht) (hywiki-maybe-directory-updated)) +(defun hywiki-completion-exit-function () + "Function called when HyWiki reference completion ends." + (hywiki-maybe-highlight-reference)) + (defun hywiki-word-add-completion-at-point () - "Add HyWikiWord in-buffer completion to `completion-at-point-functions'. + "Add HyWiki refs in-buffer completion to `completion-at-point-functions'. Completion requires typing at least the two first characters of the -completion or no completion xandidates are returned." - (add-hook 'completion-at-point-functions - #'hywiki-completion-at-point nil t)) +completion or no completion candidates are returned. +If using `company-mode', you must use the `company-capf' backend for HyWiki +completion to work properly." + (add-hook 'completion-at-point-functions #'hywiki-completion-at-point -90 t) + (cond ((bound-and-true-p corfu-mode)) ;; Uses :exit-function in hywiki-c-a-p + ((bound-and-true-p company-mode) + (add-hook 'company-completion-finished-hook + #'hywiki-completion-exit-function) + (add-hook 'company-completion-cancelled-hook + #'hywiki-completion-exit-function)) + ;; Default completion + (t (advice-add 'completion--insert :after #'hywiki-completion-exit-function)))) + +(defun hywiki-word-remove-completion-at-point () + "Remove HyWiki refs in-buffer completion from `completion-at-point-functions'." + (remove-hook 'completion-at-point-functions #'hywiki-completion-at-point t) + (remove-hook 'company-completion-finished-hook #'hywiki-completion-exit-function) + (remove-hook 'company-completion-cancelled-hook #'hywiki-completion-exit-function) + (advice-remove 'completion--insert #'hywiki-completion-exit-function)) (defun hywiki-word-highlight-buffers (buffers) "Setup HyWikiWord auto-highlighting and highlight in BUFFERS." @@ -3969,6 +4015,7 @@ completion or no completion xandidates are returned." (interactive) (dolist (buf buffers) (with-current-buffer buf + (hywiki-word-remove-completion-at-point) (remove-hook 'pre-command-hook 'hywiki-word-store-around-point :local) (remove-hook 'post-self-insert-hook 'hywiki-word-highlight-post-self-insert :local) (remove-hook 'post-command-hook 'hywiki-word-highlight-post-command :local) @@ -3983,8 +4030,8 @@ completion or no completion xandidates are returned." (defun hywiki-word-dehighlight-buffers (buffers) "Disable HyWikiWord auto-highlighting and dehighlight in BUFFERS." (interactive) - (remove-hook 'after-change-major-mode-hook 'hywiki-word-highlight-in-current-buffer) (remove-hook 'after-change-major-mode-hook 'hywiki-word-add-completion-at-point) + (remove-hook 'after-change-major-mode-hook 'hywiki-word-highlight-in-current-buffer) (remove-hook 'window-buffer-change-functions 'hywiki-word-highlight-in-frame) (setq yank-handled-properties (delete '(hywiki-word-face . hywiki-highlight-on-yank) diff --git a/test/hywconfig-tests.el b/test/hywconfig-tests.el index 82144e5f..4b6c0291 100644 --- a/test/hywconfig-tests.el +++ b/test/hywconfig-tests.el @@ -3,7 +3,7 @@ ;; Author: Mats Lidell ;; ;; Orig-Date: 30-Jan-21 at 12:00:00 -;; Last-Mod: 23-Dec-23 at 01:21:53 by Bob Weiner +;; Last-Mod: 22-Feb-26 at 23:34:03 by Bob Weiner ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -29,7 +29,7 @@ "Remove names from frame parameters." (set-frame-parameter nil 'named-hywconfigs nil)) -(ert-deftest hywconfig--inital-ring-is-empty () +(ert-deftest hywconfig--initial-ring-is-empty () "Verify an initial ring is empty." (hywconfig-tests--remove-ring) (should (hywconfig-ring-empty-p))) diff --git a/test/hywiki-yki-tests.el b/test/hywiki-yki-tests.el index 07b54254..e45e14eb 100644 --- a/test/hywiki-yki-tests.el +++ b/test/hywiki-yki-tests.el @@ -105,12 +105,6 @@ inserted. Finally a `hywiki-test--point-char' is inserted where point is." (should (string= (hywiki-test--insert-chars '((1 . 6) (7 . 12))) " \n")))) -(defun hywiki-test--insert (string) - "Command to insert a STRING at point." - (interactive "s: ") - (dolist (c (string-to-list string)) - (hywiki-tests--command-execute #'self-insert-command 1 c))) - (defun hywiki-test--insert-with-point (string) "Insert STRING and return new POINT pos given by `hywiki-test--point-char'. The point char is not inserted." @@ -132,7 +126,6 @@ buffer. End the insertion of text by turning on hywiki-mode and perform a dummy command to get the pre- and post-hooks executed. This creates the highlighting overlays we want to test." - (hywiki-mode :all) (erase-buffer) (goto-char (hywiki-test--insert-with-point description))) @@ -173,7 +166,6 @@ Each test is constructed as three phases: (should (string= stop (hywiki-test--get-buffer-text-with-point-and-highlight))))) (unwind-protect (progn - (hywiki-mode :all) (ert-info ("1" :prefix "Verify point, no highlighting:") (pre: "non^wikiword") (post: "non^wikiword")) @@ -186,12 +178,12 @@ Each test is constructed as three phases: (post: "^")) (ert-info ("4" :prefix "Verify highlighting: ") (pre: "Hi^Ho") - (hywiki-test--insert "\"text\"") - (post: "Hi\"text\"^")) + (hywiki-tests--insert-by-char "text ") + (post: "Hitext ^")) (ert-info ("5" :prefix "Verify highlighting: ") (pre: "Hi^Ho") - (hywiki-test--insert " \"text\"") - (post: " \"text\"^")) + (hywiki-tests--insert-by-char " text ") + (post: " text ^")) ;; PASS: WikiWord -> highlight {WikiWord} after delete (ert-info ("6" :prefix "Verify highlighting: ")