config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

ob-comint.el (17251B)


      1 ;;; ob-comint.el --- Babel Functions for Interaction with Comint Buffers -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2009-2024 Free Software Foundation, Inc.
      4 
      5 ;; Author: Eric Schulte
      6 ;; Keywords: literate programming, reproducible research, comint
      7 ;; URL: https://orgmode.org
      8 
      9 ;; This file is part of GNU Emacs.
     10 
     11 ;; GNU Emacs is free software: you can redistribute it and/or modify
     12 ;; it under the terms of the GNU General Public License as published by
     13 ;; the Free Software Foundation, either version 3 of the License, or
     14 ;; (at your option) any later version.
     15 
     16 ;; GNU Emacs is distributed in the hope that it will be useful,
     17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     19 ;; GNU General Public License for more details.
     20 
     21 ;; You should have received a copy of the GNU General Public License
     22 ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
     23 
     24 ;;; Commentary:
     25 
     26 ;; These functions build on comint to ease the sending and receiving
     27 ;; of commands and results from comint buffers.
     28 
     29 ;; Note that the buffers in this file are analogous to sessions in
     30 ;; org-babel at large.
     31 
     32 ;;; Code:
     33 
     34 (require 'org-macs)
     35 (org-assert-version)
     36 
     37 (require 'ob-core)
     38 (require 'org-compat)
     39 (require 'comint)
     40 
     41 (defun org-babel-comint-buffer-livep (buffer)
     42   "Check if BUFFER is a comint buffer with a live process."
     43   (let ((buffer (when buffer (get-buffer buffer))))
     44     (and buffer (buffer-live-p buffer) (get-buffer-process buffer) buffer)))
     45 
     46 (defmacro org-babel-comint-in-buffer (buffer &rest body)
     47   "Check BUFFER and execute BODY.
     48 BUFFER is checked with `org-babel-comint-buffer-livep'.  BODY is
     49 executed inside the protection of `save-excursion' and
     50 `save-match-data'."
     51   (declare (indent 1) (debug t))
     52   `(progn
     53      (unless (org-babel-comint-buffer-livep ,buffer)
     54        (error "Buffer %s does not exist or has no process" ,buffer))
     55      (save-match-data
     56        (with-current-buffer ,buffer
     57 	 (save-excursion
     58 	   (let ((comint-input-filter (lambda (_input) nil)))
     59 	     ,@body))))))
     60 
     61 (defvar-local org-babel-comint-prompt-regexp-old nil
     62   "Fallback regexp used to detect prompt.")
     63 
     64 (defcustom org-babel-comint-fallback-regexp-threshold 5.0
     65   "Waiting time until trying to use fallback regexp to detect prompt.
     66 This is useful when prompt unexpectedly changes."
     67   :type 'float
     68   :group 'org-babel
     69   :package-version '(Org . "9.7"))
     70 
     71 (defun org-babel-comint--set-fallback-prompt ()
     72   "Swap `comint-prompt-regexp' and `org-babel-comint-prompt-regexp-old'."
     73   (when org-babel-comint-prompt-regexp-old
     74     (let ((tmp comint-prompt-regexp))
     75       (setq comint-prompt-regexp org-babel-comint-prompt-regexp-old
     76             org-babel-comint-prompt-regexp-old tmp))))
     77 
     78 (defun org-babel-comint--prompt-filter (string &optional prompt-regexp)
     79   "Remove PROMPT-REGEXP from STRING.
     80 
     81 PROMPT-REGEXP defaults to `comint-prompt-regexp'."
     82   (let* ((prompt-regexp (or prompt-regexp comint-prompt-regexp))
     83          ;; We need newline in case if we do progressive replacement
     84          ;; of agglomerated comint prompts with `comint-prompt-regexp'
     85          ;; containing ^.
     86          (separator "org-babel-comint--prompt-filter-separator\n"))
     87     (while (string-match-p prompt-regexp string)
     88       (setq string
     89             (replace-regexp-in-string
     90              (format "\\(?:%s\\)?\\(?:%s\\)[ \t]*" separator prompt-regexp)
     91              separator string)))
     92     (delete "" (split-string string separator))))
     93 
     94 (defun org-babel-comint--echo-filter (string &optional echo)
     95   "Remove ECHO from STRING."
     96   (and echo string
     97        (string-match
     98         (replace-regexp-in-string "\n" "[\r\n]+" (regexp-quote echo))
     99         string)
    100        (setq string (substring string (match-end 0))))
    101   string)
    102 
    103 (defmacro org-babel-comint-with-output (meta &rest body)
    104   "Evaluate BODY in BUFFER and return process output.
    105 Will wait until EOE-INDICATOR appears in the output, then return
    106 all process output.  If REMOVE-ECHO and FULL-BODY are present and
    107 non-nil, then strip echo'd body from the returned output.  META
    108 should be a list containing the following where the last two
    109 elements are optional.
    110 
    111  (BUFFER EOE-INDICATOR REMOVE-ECHO FULL-BODY)
    112 
    113 This macro ensures that the filter is removed in case of an error
    114 or user `keyboard-quit' during execution of body."
    115   (declare (indent 1) (debug (sexp body)))
    116   (let ((buffer (nth 0 meta))
    117 	(eoe-indicator (nth 1 meta))
    118 	(remove-echo (nth 2 meta))
    119 	(full-body (nth 3 meta)))
    120     `(org-babel-comint-in-buffer ,buffer
    121        (let* ((string-buffer "")
    122 	      (comint-output-filter-functions
    123 	       (cons (lambda (text)
    124                        (setq string-buffer (concat string-buffer text)))
    125 		     comint-output-filter-functions))
    126 	      dangling-text)
    127 	 ;; got located, and save dangling text
    128 	 (goto-char (process-mark (get-buffer-process (current-buffer))))
    129 	 (let ((start (point))
    130 	       (end (point-max)))
    131 	   (setq dangling-text (buffer-substring start end))
    132 	   (delete-region start end))
    133 	 ;; pass FULL-BODY to process
    134 	 ,@body
    135 	 ;; wait for end-of-evaluation indicator
    136          (let ((start-time (current-time)))
    137 	   (while (progn
    138 		    (goto-char comint-last-input-end)
    139 		    (not (save-excursion
    140 		         (and (re-search-forward
    141 			       (regexp-quote ,eoe-indicator) nil t)
    142 			      (re-search-forward
    143 			       comint-prompt-regexp nil t)))))
    144 	     (accept-process-output
    145               (get-buffer-process (current-buffer))
    146               org-babel-comint-fallback-regexp-threshold)
    147              (when (and org-babel-comint-prompt-regexp-old
    148                         (> (float-time (time-since start-time))
    149                            org-babel-comint-fallback-regexp-threshold)
    150                         (progn
    151 		          (goto-char comint-last-input-end)
    152 		          (save-excursion
    153                             (and
    154                              (re-search-forward
    155 			      (regexp-quote ,eoe-indicator) nil t)
    156 			     (re-search-forward
    157                               org-babel-comint-prompt-regexp-old nil t)))))
    158                (org-babel-comint--set-fallback-prompt))))
    159 	 ;; replace cut dangling text
    160 	 (goto-char (process-mark (get-buffer-process (current-buffer))))
    161 	 (insert dangling-text)
    162 
    163          ;; remove echo'd FULL-BODY from input
    164          (and ,remove-echo ,full-body
    165               (setq string-buffer (org-babel-comint--echo-filter string-buffer ,full-body)))
    166 
    167          ;; Filter out prompts.
    168          (org-babel-comint--prompt-filter string-buffer)))))
    169 
    170 (defun org-babel-comint-input-command (buffer cmd)
    171   "Pass CMD to BUFFER.
    172 The input will not be echoed."
    173   (org-babel-comint-in-buffer buffer
    174     (goto-char (process-mark (get-buffer-process buffer)))
    175     (insert cmd)
    176     (comint-send-input)
    177     (org-babel-comint-wait-for-output buffer)))
    178 
    179 (defun org-babel-comint-wait-for-output (buffer)
    180   "Wait until output arrives from BUFFER.
    181 Note: this is only safe when waiting for the result of a single
    182 statement (not large blocks of code)."
    183   (org-babel-comint-in-buffer buffer
    184     (let ((start-time (current-time)))
    185       (while (progn
    186                (goto-char comint-last-input-end)
    187                (not (and (re-search-forward comint-prompt-regexp nil t)
    188                        (goto-char (match-beginning 0)))))
    189         (accept-process-output
    190          (get-buffer-process buffer)
    191          org-babel-comint-fallback-regexp-threshold)
    192         (when (and org-babel-comint-prompt-regexp-old
    193                    (> (float-time (time-since start-time))
    194                       org-babel-comint-fallback-regexp-threshold)
    195                    (progn
    196 		     (goto-char comint-last-input-end)
    197 		     (save-excursion
    198 		       (re-search-forward
    199                         org-babel-comint-prompt-regexp-old nil t))))
    200           (org-babel-comint--set-fallback-prompt))))))
    201 
    202 (defun org-babel-comint-eval-invisibly-and-wait-for-file
    203     (buffer file string &optional period)
    204   "Evaluate STRING in BUFFER invisibly.
    205 Don't return until FILE exists.  Code in STRING must ensure that
    206 FILE exists at end of evaluation."
    207   (unless (org-babel-comint-buffer-livep buffer)
    208     (error "Buffer %s does not exist or has no process" buffer))
    209   (when (file-exists-p file) (delete-file file))
    210   (process-send-string
    211    (get-buffer-process buffer)
    212    (if (= (aref string (1- (length string))) ?\n) string (concat string "\n")))
    213   (while (not (file-exists-p file)) (sit-for (or period 0.25))))
    214 
    215 
    216 ;;; Async evaluation
    217 
    218 (defvar-local org-babel-comint-async-indicator nil
    219   "Regular expression that `org-babel-comint-async-filter' scans for.
    220 It should have 2 parenthesized expressions,
    221 e.g. \"org_babel_async_\\(start\\|end\\|file\\)_\\(.*\\)\".  The
    222 first parenthesized expression determines whether the token is
    223 delimiting a result block, or whether the result is in a file.
    224 If delimiting a block, the second expression gives a UUID for the
    225 location to insert the result.  Otherwise, the result is in a tmp
    226 file, and the second expression gives the file name.")
    227 
    228 (defvar-local org-babel-comint-async-buffers nil
    229   "List of Org mode buffers to check for Babel async output results.")
    230 
    231 (defvar-local org-babel-comint-async-file-callback nil
    232   "Callback to clean and insert Babel async results from a temp file.
    233 The callback function takes two arguments: the alist of params of the Babel
    234 source block, and the name of the temp file.")
    235 
    236 (defvar-local org-babel-comint-async-chunk-callback nil
    237   "Callback function to clean Babel async output results before insertion.
    238 Its single argument is a string consisting of output from the
    239 comint process.  It should return a string that will be passed
    240 to `org-babel-insert-result'.")
    241 
    242 (defvar-local org-babel-comint-async-remove-prompts-p t
    243   "Whether prompts should be detected and removed from async output.")
    244 
    245 (defvar-local org-babel-comint-async-dangling nil
    246   "Dangling piece of the last process output, as a string.
    247 Used when `org-babel-comint-async-indicator' is spread across multiple
    248 comint outputs due to buffering.")
    249 
    250 (defun org-babel-comint-use-async (params)
    251   "Determine whether to use session async evaluation.
    252 PARAMS are the header arguments as passed to
    253 `org-babel-execute:lang'."
    254   (let ((async (assq :async params))
    255         (session (assq :session params)))
    256     (and async
    257 	 (not org-babel-exp-reference-buffer)
    258          (not (equal (cdr async) "no"))
    259          (not (equal (cdr session) "none")))))
    260 
    261 (defun org-babel-comint-async-filter (string)
    262   "Captures Babel async output from comint buffer back to Org mode buffers.
    263 This function is added as a hook to `comint-output-filter-functions'.
    264 STRING contains the output originally inserted into the comint buffer."
    265   ;; Remove outdated Org mode buffers
    266   (setq org-babel-comint-async-buffers
    267 	(cl-loop for buf in org-babel-comint-async-buffers
    268 	         if (buffer-live-p buf)
    269 	         collect buf))
    270   (let* ((indicator org-babel-comint-async-indicator)
    271 	 (org-buffers org-babel-comint-async-buffers)
    272 	 (file-callback org-babel-comint-async-file-callback)
    273 	 (combined-string (concat org-babel-comint-async-dangling string))
    274 	 (new-dangling combined-string)
    275          ;; Assumes comint filter called with session buffer current
    276          (session-dir default-directory)
    277 	 ;; list of UUID's matched by `org-babel-comint-async-indicator'
    278 	 uuid-list)
    279     (with-temp-buffer
    280       (insert combined-string)
    281       (goto-char (point-min))
    282       (while (re-search-forward indicator nil t)
    283 	;; update dangling
    284 	(setq new-dangling (buffer-substring (point) (point-max)))
    285 	(cond ((equal (match-string 1) "end")
    286 	       ;; save UUID for insertion later
    287 	       (push (match-string 2) uuid-list))
    288 	      ((equal (match-string 1) "file")
    289 	       ;; insert results from tmp-file
    290 	       (let ((tmp-file (match-string 2)))
    291 		 (cl-loop for buf in org-buffers
    292 		          until
    293 		          (with-current-buffer buf
    294 			    (save-excursion
    295 			      (goto-char (point-min))
    296 			      (when (search-forward tmp-file nil t)
    297 			        (org-babel-previous-src-block)
    298                                 (let* ((info (org-babel-get-src-block-info))
    299                                        (params (nth 2 info))
    300                                        (result-params
    301                                         (cdr (assq :result-params params)))
    302                                        (default-directory session-dir))
    303                                   (org-babel-insert-result
    304                                    (funcall file-callback
    305                                             (nth
    306                                              2 (org-babel-get-src-block-info))
    307                                             tmp-file)
    308                                    result-params info))
    309 			        t))))))))
    310       ;; Truncate dangling to only the most recent output
    311       (when (> (length new-dangling) (length string))
    312 	(setq new-dangling string)))
    313     (setq-local org-babel-comint-async-dangling new-dangling)
    314     (when uuid-list
    315       ;; Search for results in the comint buffer
    316       (save-excursion
    317 	(goto-char (point-max))
    318 	(while uuid-list
    319 	  (re-search-backward indicator)
    320 	  (when (equal (match-string 1) "end")
    321 	    (let* ((uuid (match-string-no-properties 2))
    322 		   (res-str-raw
    323 		    (buffer-substring
    324 		     ;; move point to beginning of indicator
    325                      (match-beginning 0)
    326 		     ;; find the matching start indicator
    327 		     (cl-loop
    328                       do (re-search-backward indicator)
    329 		      until (and (equal (match-string 1) "start")
    330 				 (equal (match-string 2) uuid))
    331 		      finally return (+ 1 (match-end 0)))))
    332 		   ;; Apply user callback
    333 		   (res-str (funcall org-babel-comint-async-chunk-callback
    334                                      (if org-babel-comint-async-remove-prompts-p
    335                                          (org-trim (string-join
    336                                                     (mapcar #'org-trim
    337                                                             (org-babel-comint--prompt-filter
    338                                                              res-str-raw))
    339                                                     "\n")
    340                                                    t)
    341                                        res-str-raw))))
    342 	      ;; Search for uuid in associated org-buffers to insert results
    343 	      (cl-loop for buf in org-buffers
    344 		       until (with-current-buffer buf
    345 			       (save-excursion
    346 			         (goto-char (point-min))
    347 			         (when (search-forward uuid nil t)
    348 				   (org-babel-previous-src-block)
    349                                    (let* ((info (org-babel-get-src-block-info))
    350                                           (params (nth 2 info))
    351                                           (result-params
    352                                            (cdr (assq :result-params params)))
    353                                           (default-directory session-dir))
    354 				     (org-babel-insert-result
    355                                       res-str result-params info))
    356 				   t))))
    357 	      ;; Remove uuid from the list to search for
    358 	      (setq uuid-list (delete uuid uuid-list)))))))))
    359 
    360 (defun org-babel-comint-async-register
    361     (session-buffer org-buffer indicator-regexp
    362 		    chunk-callback file-callback
    363                     &optional prompt-handling)
    364   "Set local org-babel-comint-async variables in SESSION-BUFFER.
    365 ORG-BUFFER is added to `org-babel-comint-async-buffers' if not
    366 present.  `org-babel-comint-async-indicator',
    367 `org-babel-comint-async-chunk-callback', and
    368 `org-babel-comint-async-file-callback' are set to
    369 INDICATOR-REGEXP, CHUNK-CALLBACK, and FILE-CALLBACK respectively.
    370 PROMPT-HANDLING may be either of the symbols `filter-prompts', in
    371 which case prompts matching `comint-prompt-regexp' are filtered
    372 from output before it is passed to CHUNK-CALLBACK, or
    373 `disable-prompt-filtering', in which case this behavior is
    374 disabled.  For backward-compatibility, the default value of `nil'
    375 is equivalent to `filter-prompts'."
    376   (org-babel-comint-in-buffer session-buffer
    377     (setq org-babel-comint-async-indicator indicator-regexp
    378 	  org-babel-comint-async-chunk-callback chunk-callback
    379 	  org-babel-comint-async-file-callback file-callback)
    380     (setq org-babel-comint-async-remove-prompts-p
    381           (cond
    382            ((eq prompt-handling 'disable-prompt-filtering) nil)
    383            ((eq prompt-handling 'filter-prompts) t)
    384            ((eq prompt-handling nil) t)
    385            (t (error (format "Unrecognized prompt handling behavior %s"
    386                              prompt-handling)))))
    387     (unless (memq org-buffer org-babel-comint-async-buffers)
    388       (setq org-babel-comint-async-buffers
    389 	    (cons org-buffer org-babel-comint-async-buffers)))
    390     (add-hook 'comint-output-filter-functions
    391 	      'org-babel-comint-async-filter nil t)))
    392 
    393 (defmacro org-babel-comint-async-delete-dangling-and-eval
    394     (session-buffer &rest body)
    395   "Remove dangling text in SESSION-BUFFER and evaluate BODY.
    396 This is analogous to `org-babel-comint-with-output', but meant
    397 for asynchronous output, and much shorter because inserting the
    398 result is delegated to `org-babel-comint-async-filter'."
    399   (declare (indent 1) (debug t))
    400   `(org-babel-comint-in-buffer ,session-buffer
    401      (goto-char (process-mark (get-buffer-process (current-buffer))))
    402      (delete-region (point) (point-max))
    403      ,@body))
    404 
    405 (provide 'ob-comint)
    406 
    407 
    408 
    409 ;;; ob-comint.el ends here