diff --git a/ChangeLog b/ChangeLog index c67a3ab3..dcdc5c8a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,16 @@ 2026-03-29 Bob Weiner +* hywiki.el (consult--async-*): Silence byte-compiler warnings with function + declarations. + +* hypb.el (hypb:in-string-cache-disable, hypb:in-string-cache): Add. + (hypb:in-string-check): Rename from 'hypb:in-string-p' so can be + a subfunction call when cache lookup fails. Rewrite 'max-lines' handling + to use in-buffer ranges rather than strings. + (hypb:in-string-p): Rewrite to utilize caching. + (hypb:narrow-to-max-lines): Add for potential future use and to archive + prior code from 'hypb:in-string-p'. + * kotl/kview.el (kview:id-counter-set): Add. kotl/kotl-mode.el (kotl-mode:kill-tree): Add support for sending 0 as the prefix arg to say kill from the root of the outline tree all the @@ -93,6 +104,13 @@ (hywiki-org-to-heading-instance): Use with-suppressed-warnings for obsolete org-show-entry. +* hywiki.el (hywiki-delimited-p): Fix to return non-nil only when the (car range) + is non-nil since range may be '(nil ) when a cached entry is + found. + (hywiki-get-page-files): Fix to test whether 'hywiki-directory' exists + before calling 'make-directory' to avoid its call to 'make-directory-insernal' + triggering an error when debugging. + 2026-03-24 Bob Weiner * hibtypes.el (hib-link-to-file-line): Add missing 'let' for 'ext' variable, diff --git a/hypb.el b/hypb.el index 7b0ad328..834e9a1b 100644 --- a/hypb.el +++ b/hypb.el @@ -3,7 +3,7 @@ ;; Author: Bob Weiner ;; ;; Orig-Date: 6-Oct-91 at 03:42:38 -;; Last-Mod: 28-Mar-26 at 11:58:58 by Bob Weiner +;; Last-Mod: 29-Mar-26 at 19:02:41 by Bob Weiner ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -68,6 +68,9 @@ "Prefix attached to all native Hyperbole help buffer names. This should end with a space.") +(defvar hypb:in-string-cache-disable nil + "When non-nil, disable use of `hypb:in-string-p' caching.") + (defcustom hypb:in-string-mode-regexps '(if (derived-mode-p 'texinfo-mode) '(("``\\|\"" @@ -723,15 +726,14 @@ This will this install the Emacs helm package when needed." (error "(hypb:hkey-help-file): Non-existent file: \"%s\"" help-file)))) -(defvar hypb:in-string-and-tick (cons nil 0)) - (defun hypb:in-string-p (&optional max-lines range-flag) - "Return non-nil iff point is within a string and not on the closing quote. + "Return (is-in-string start end) for point, cached by buffer & modified time. +Return non-nil iff point is within a string and not on the closing quote. With optional MAX-LINES, an integer, match only within that many lines from point. With optional RANGE-FLAG when there is a -match, return list of (string-matched start-pos end-pos), where -the positions exclude the delimiters. +match, return list of (in-string-flag start-pos end-pos), where +in-string-flag is t or nil and the positions exclude the delimiters. To prevent searching back to the buffer start and producing slow performance, this limits its count of quotes found prior to point @@ -739,6 +741,116 @@ to the beginning of the first line prior to point that contains a non-backslashed quote mark and limits string length to a maximum of 9000 characters. +Quoting conventions recognized are: + double-quotes: \"str\"; + Markdown triple backticks: ```str```; + Python single-quotes: \\='str\\='; + Python triple single-quotes: '''str'''; + Python triple double-quotes: \"\"\"str\"\"\"; + Texinfo open and close quotes: ``str''." + + (unless (and (integerp max-lines) (<= max-lines 0)) + (let* ((buf (current-buffer)) + (tick (buffer-chars-modified-tick)) + (pos (point)) + (cache-entry (gethash buf hypb:in-string-cache))) + + ;; If buffer mod tick has changed, empty the buffer's string range cache + (when (or (not cache-entry) (/= tick (car cache-entry))) + (setq cache-entry (list tick)) ; Start a new list with just the tick + (puthash buf cache-entry hypb:in-string-cache)) + + ;; Check for a cache match range. The cdr of `cache-entry' is a list of + ;; (in-str start end) ranges. + (let ((match (cl-find-if (lambda (r) + (and (>= pos (or (nth 1 r) (point-max))) + (<= pos (or (nth 2 r) (point-min))))) + (cdr cache-entry))) + cache-match) + (when match + ;; Cache match + (setq cache-match match)) + + ;; When no matching cache entry, search for a string match + (let* ((entry (or cache-match + ;; This ignores max-lines, so final result of + ;; whether in a string with max-lines may differ + ;; from what this returns. This allows its result + ;; to be used in the cache. + ;; To call it with the buffer narrowed to + ;; according to `max-lines', use: + ;; (hypb:narrow-to-max-lines max-lines #'hypb:in-string-check t) + (hypb:in-string-check t))) + (in-str (nth 0 entry)) + (str-start (nth 1 entry)) + (str-end (nth 2 entry))) + (cond ((and (not match) (not hypb:in-string-cache-disable)) + ;; Add the new range into the buffer's cache + (setcdr cache-entry (cons entry (cdr cache-entry)))) + (match) + ((not entry) + (setq hypb:in-string-cache-disable t) + (error "(hypb:in-string-p): buffer = %s; cache-match = %S; entry = %s; point = %d" + (current-buffer) cache-match entry (point)))) + ;; Ignore if more than `max-lines' matched + (when (and in-str str-start str-end + (or (null max-lines) + (and (integerp max-lines) + ;; When computing the number of lines in + ;; the string match, ignore any leading and + ;; trailing newlines. This allows for + ;; opening and closing quotes to be on + ;; separate lines, useful with multi-line + ;; strings. + (let (newlines) + (or (< (setq newlines (count-matches "\n" str-start str-end)) + max-lines) + (< (save-excursion + (- newlines + (progn (goto-char str-start) + (if (looking-at "[\n\r\f\t ]+") + (count-matches "\n" (match-beginning 0) (match-end 0)) + 0)) + (progn (goto-char str-end) + (if (zerop (skip-chars-backward "\n\r\f\t ")) + 0 + (count-matches "\n" (point) str-end))))) + max-lines)))))) + (if range-flag + (cond ((zerop str-start) + (list nil (point) (point))) + (in-str (list (buffer-substring-no-properties str-start str-end) str-start str-end))) + in-str))))))) + +(defun hypb:narrow-to-max-lines (max-lines func &rest args) + "Narrow buffer to +/- (+ 1 MAX-LINES) including current line. +Then call FUNC with rest of ARGS and afterwards restore prior narrowing and +prior point." + (save-excursion + (save-restriction + (when (integerp max-lines) + (if (zerop max-lines) + (narrow-to-region (point) (point)) ;; Empty range + ;; Allow for +/- (+ 1 max-lines) including current line so start + ;; and end delimiters can be on separate lines. Before returning, + ;; this function checks that any matched string has <= max-lines. + (narrow-to-region (line-beginning-position + (when max-lines (1+ (- max-lines)))) + (line-end-position (1+ max-lines))))) + (apply func args)))) + +(defun hypb:in-string-check (&optional range-flag) + "Return non-nil iff point is within a string and not on the closing quote. + +With optional RANGE-FLAG when there is a match, return list of (in-string-flag +start-pos end-pos), where in-string-flag is t or nil and the positions exclude +the delimiters. + +To prevent searching back to the buffer start and producing slow +performance, this limits its count of quotes found prior to point to the +beginning of the first line prior to point that contains a non-backslashed +quote mark and limits string length to a maximum of 9000 characters. + Quoting conventions recognized are: double-quotes: \"str\"; Markdown triple backticks: ```str```; @@ -767,123 +879,102 @@ Quoting conventions recognized are: list-of-unformatted-open-close-regexps))) (error "(hypb:in-string-p): `hypb:in-string-mode-regexps' must be a list of lists, not %S" hypb:in-string-mode-regexps)) - (save-excursion - (save-restriction - (when (integerp max-lines) - (if (zerop max-lines) - (narrow-to-region (point) (point)) ;; Empty range - ;; Allow for +/- (+ 1 max-lines) including current line so start - ;; and end delimiters can be on separate lines. Before returning, - ;; this function checks that any matched string has <= max-lines. - (narrow-to-region (line-beginning-position - (when max-lines (1+ (- max-lines)))) - (line-end-position (1+ max-lines))))) - (cl-block :result - (let ((opoint (point)) - (start (point-min))) - (dolist (open-close-regexps list-of-open-close-regexps) - (save-excursion - ;; Don't use `syntax-ppss' here as it fails to ignore backquoted - ;; double quote characters in strings and doesn't work in - ;; `change-log-mode' due to its syntax-table. - (let ((open-match-string "") - possible-delim - str - str-start - str-end) - (cl-destructuring-bind (orig-open-regexp orig-close-regexp open-regexp _close-regexp) - open-close-regexps - (save-match-data - (if (and (setq possible-delim - (or (looking-at orig-open-regexp) - (looking-at orig-close-regexp))) - (/= (or (char-before) 0) ?\\) - (setq open-match-string (match-string 2))) - (while (and (setq possible-delim (search-backward open-match-string (max (point-min) (- (point) limit)) t)) - (if (= (or (char-before) 0) ?\\) - (goto-char (1- (point))) - (progn (setq str-start (match-end 0)) - nil)))) - (when (setq possible-delim (re-search-backward open-regexp (max (point-min) (- (point) limit)) t)) - (setq open-match-string (match-string 2)) - (setq str-start (match-end 2)))) - - (when (and possible-delim - str-start - ;; If this is the start of a string, it must be - ;; at the start of line, preceded by whitespace - ;; or preceded by another string end sequence. - ;; (save-match-data - ;; (or (string-empty-p (match-string 1)) - ;; (string-search (match-string 1) " \t\n\r\f") - ;; (progn (goto-char (1+ (point))) - ;; (looking-back close-regexp nil)))) - ) - (forward-line 0) - (setq start (point)) - (goto-char opoint) - (if (and (derived-mode-p 'texinfo-mode) - (string-equal open-match-string texinfo-open-quote)) - (and (cl-oddp (- (count-matches (regexp-quote open-match-string) - start (point)) - ;; Subtract any backslash quoted delimiters - (count-matches - (format "[\\]\\(%s\\)" - (regexp-quote open-match-string)) - start (point)) - (count-matches (regexp-quote texinfo-close-quote) - start (point)) - ;; Subtract any backslash quoted delimiters - (count-matches - (format "[\\]\\(%s\\)" - (regexp-quote texinfo-close-quote)) - start (point)))) - - (progn (while (and (setq possible-delim (search-forward - texinfo-close-quote - (min (point-max) (+ (point) limit)) - t)) - (= (or (char-before (match-beginning 0)) 0) ?\\))) - possible-delim) - (setq str-end (match-beginning 0) - str (buffer-substring-no-properties str-start str-end))) - (and (cl-oddp (- (count-matches (regexp-quote open-match-string) - start (point)) - ;; Subtract any backslash quoted delimiters - (count-matches - (format "[\\]\\(%s\\)" - (regexp-quote open-match-string)) - start (point)))) - ;; Move back one char in case point is on a - ;; closing delimiter char to ensure it is not - ;; backslash quoted and so the right delimiter is matched. - ;; Find the matching closing delimiter - (progn (while (and (setq possible-delim - (search-forward open-match-string - (min (point-max) (+ (point) limit)) - t)) - (= (or (char-before (match-beginning 0)) 0) ?\\))) - possible-delim) - (setq str-end (match-beginning 0)) - (setq str (buffer-substring-no-properties str-start str-end)))) - - ;; Ignore if more than `max-lines' matched - (when (and str - (or (null max-lines) - (and (integerp max-lines) - ;; When computing the number of lines in - ;; the string match, ignore any leading and - ;; trailing newlines. This allows for - ;; opening and closing quotes to be on - ;; separate lines, useful with multi-line - ;; strings. - (< (hypb:string-count-matches - "\n" (string-trim str)) - max-lines)))) - (cl-return-from :result - (if range-flag - (list str str-start str-end) - t))))))))))))))) + (cl-block :result + (let ((opoint (point)) + (start (point-min))) + (dolist (open-close-regexps list-of-open-close-regexps) + (save-excursion + ;; Don't use `syntax-ppss' here as it fails to ignore backquoted + ;; double quote characters in strings and doesn't work in + ;; `change-log-mode' due to its syntax-table. + (let ((open-match-string "") + possible-delim + in-str + str-start + str-end) + (cl-destructuring-bind (orig-open-regexp orig-close-regexp open-regexp _close-regexp) + open-close-regexps + (save-match-data + (if (and (setq possible-delim + (or (looking-at orig-open-regexp) + (looking-at orig-close-regexp))) + (/= (or (char-before) 0) ?\\) + (setq open-match-string (match-string 2))) + (while (and (setq possible-delim (search-backward open-match-string (max (point-min) (- (point) limit)) t)) + (if (= (or (char-before) 0) ?\\) + (goto-char (1- (point))) + (progn (setq str-start (match-end 0)) + nil)))) + (when (setq possible-delim (re-search-backward open-regexp (max (point-min) (- (point) limit)) t)) + (setq open-match-string (match-string 2)) + (setq str-start (match-end 2)))) + + (when (and possible-delim + str-start + ;; If this is the start of a string, it must be + ;; at the start of line, preceded by whitespace + ;; or preceded by another string end sequence. + ;; (save-match-data + ;; (or (string-empty-p (match-string 1)) + ;; (string-search (match-string 1) " \t\n\r\f") + ;; (progn (goto-char (1+ (point))) + ;; (looking-back close-regexp nil)))) + ) + (forward-line 0) + (setq start (point)) + (goto-char opoint) + (if (and (derived-mode-p 'texinfo-mode) + (string-equal open-match-string texinfo-open-quote)) + (and (cl-oddp (- (count-matches (regexp-quote open-match-string) + start (point)) + ;; Subtract any backslash quoted delimiters + (count-matches + (format "[\\]\\(%s\\)" + (regexp-quote open-match-string)) + start (point)) + (count-matches (regexp-quote texinfo-close-quote) + start (point)) + ;; Subtract any backslash quoted delimiters + (count-matches + (format "[\\]\\(%s\\)" + (regexp-quote texinfo-close-quote)) + start (point)))) + + (progn (while (and (setq possible-delim (search-forward + texinfo-close-quote + (min (point-max) (+ (point) limit)) + t)) + (= (or (char-before (match-beginning 0)) 0) ?\\))) + possible-delim) + (setq str-end (match-beginning 0) + in-str t)) + (and (cl-oddp (- (count-matches (regexp-quote open-match-string) + start (point)) + ;; Subtract any backslash quoted delimiters + (count-matches + (format "[\\]\\(%s\\)" + (regexp-quote open-match-string)) + start (point)))) + ;; Move back one char in case point is on a + ;; closing delimiter char to ensure it is not + ;; backslash quoted and so the right delimiter is matched. + ;; Find the matching closing delimiter + (progn (while (and (setq possible-delim + (search-forward open-match-string + (min (point-max) (+ (point) limit)) + t)) + (= (or (char-before (match-beginning 0)) 0) ?\\))) + possible-delim) + (setq str-end (match-beginning 0) + in-str t))) + + (when (and in-str str-start str-end) + (cl-return-from :result + (if range-flag + (if (or (null str-start) (zerop str-start)) + (list nil (point) (point)) + (list in-str str-start str-end)) + in-str))))))))))))) (defun hypb:indirect-function (obj) "Return the function at the end of OBJ's function chain. @@ -1435,6 +1526,10 @@ Without file, the banner is prepended to the current buffer." ;;; Private variables ;;; ************************************************************************ +(defvar hypb:in-string-cache (make-hash-table :test 'eq) + "Key: buffer, Value: (tick (range1) (range2) ...) +Where each range is ((start . end) . is-in-string).") + (define-button-type 'hyperbole-banner) (provide 'hypb) diff --git a/hywiki.el b/hywiki.el index 459a0e5a..f1537ded 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: 22-Mar-26 at 12:53:00 by Bob Weiner +;; Last-Mod: 29-Mar-26 at 19:15:13 by Bob Weiner ;; ;; SPDX-License-Identifier: GPL-3.0-or-later ;; @@ -173,6 +173,11 @@ (declare-function bookmark-completing-read "bookmark" (prompt &optional default)) (declare-function bookmark-location "bookmark" (bookmark-name-or-record)) (declare-function consult--async-command "ext:consult") +(declare-function consult--async-process "ext:consult") +(declare-function consult--async-refresh-timer "ext:consult") +(declare-function consult--async-sink "ext:consult") +(declare-function consult--async-split "ext:consult") +(declare-function consult--async-throttle "ext:consult") (declare-function consult--lookup-member "ext:consult") (declare-function consult--read "ext:consult") (declare-function hsys-org-at-tags-p "hsys-org") @@ -2160,11 +2165,10 @@ Works even when called from non-Org buffers." Include: (), {}, <>, [] and \"\" (double quotes). Exclude Org links and radio targets. -Range is limited to the previous, current and next lines, as HyWikiWord -references are limited to two lines maximum. - -If no such range, return \\='(nil nil). -This includes the delimiters: (), {}, <>, [] and \"\" (double quotes)." +Range is a list of (start end) positions or if no such range, then \\='(nil +nil). It is limited to the previous, current and next lines, as HyWikiWord +references are limited to two lines maximum. The range is inclusive of the +delimiters: (), {}, <>, [] and \"\" (double quotes)." (save-excursion (save-restriction ;; Limit balanced pair checks to previous through next lines for @@ -3024,7 +3028,8 @@ not contain a directory path or returns nil." "Return the list of existing HyWiki page file names. These must end with `hywiki-file-suffix'." (when (stringp hywiki-directory) - (make-directory hywiki-directory t) + (unless (file-directory-p hywiki-directory) + (make-directory hywiki-directory t)) (when (file-readable-p hywiki-directory) (directory-files hywiki-directory nil (concat "^" hywiki-word-regexp @@ -4088,10 +4093,11 @@ a HyWikiWord at point." (if (and wikiword (string-match "[ \t\n\r\f]+\\'" wikiword)) ;; Strip any trailing whitespace (setq wikiword-trimmed (substring wikiword 0 (match-beginning 0)) - range-trimmed (list wikiword-trimmed (nth 1 range) - (- (nth 2 range) (length (match-string - 0 wikiword))))) - (setq range-trimmed range)) + range-trimmed (when (car range) + (list wikiword-trimmed (nth 1 range) + (- (nth 2 range) (length (match-string + 0 wikiword)))))) + (setq range-trimmed (when (car range) range))) (and range-trimmed ;; Ensure closing delimiter is a match for the opening one (or (eq (matching-paren (char-before (nth 1 range)))