gptel-org.el (26527B)
1 ;;; gptel-org.el --- Org functions for gptel -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2024 Karthik Chikmagalur 4 5 ;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com> 6 ;; Keywords: 7 8 ;; This program is free software; you can redistribute it and/or modify 9 ;; it under the terms of the GNU General Public License as published by 10 ;; the Free Software Foundation, either version 3 of the License, or 11 ;; (at your option) any later version. 12 13 ;; This program is distributed in the hope that it will be useful, 14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 ;; GNU General Public License for more details. 17 18 ;; You should have received a copy of the GNU General Public License 19 ;; along with this program. If not, see <https://www.gnu.org/licenses/>. 20 21 ;;; Commentary: 22 23 ;; 24 25 ;;; Code: 26 (eval-when-compile (require 'cl-lib)) 27 (require 'org-element) 28 (require 'outline) 29 30 ;; Functions used for saving/restoring gptel state in Org buffers 31 (defvar gptel--num-messages-to-send) 32 (defvar org-entry-property-inherited-from) 33 (defvar gptel-backend) 34 (defvar gptel--known-backends) 35 (defvar gptel--system-message) 36 (defvar gptel-model) 37 (defvar gptel-temperature) 38 (defvar gptel-max-tokens) 39 40 (defvar org-link-angle-re) 41 (defvar org-link-bracket-re) 42 (declare-function mailcap-file-name-to-mime-type "mailcap") 43 (declare-function gptel--model-capable-p "gptel") 44 (declare-function gptel--model-mime-capable-p "gptel") 45 (declare-function gptel--model-name "gptel") 46 (declare-function gptel--to-string "gptel") 47 (declare-function gptel--to-number "gptel") 48 (declare-function gptel--intern "gptel") 49 (declare-function gptel--get-buffer-bounds "gptel") 50 (declare-function gptel-backend-name "gptel") 51 (declare-function gptel--parse-buffer "gptel") 52 (declare-function org-entry-get "org") 53 (declare-function org-entry-put "org") 54 (declare-function org-with-wide-buffer "org-macs") 55 (declare-function org-set-property "org") 56 (declare-function org-property-values "org") 57 (declare-function org-open-line "org") 58 (declare-function org-at-heading-p "org") 59 (declare-function org-get-heading "org") 60 (declare-function org-at-heading-p "org") 61 62 ;; Bundle `org-element-lineage-map' if it's not available (for Org 9.67 or older) 63 (eval-and-compile 64 (if (fboundp 'org-element-lineage-map) 65 (progn (declare-function org-element-lineage-map "org-element-ast") 66 (defalias 'gptel-org--element-lineage-map 'org-element-lineage-map)) 67 (defun gptel-org--element-lineage-map (datum fun &optional types with-self first-match) 68 "Map FUN across ancestors of DATUM, from closest to furthest. 69 70 DATUM is an object or element. For TYPES, WITH-SELF and 71 FIRST-MATCH see `org-element-lineage-map'. 72 73 This function is provided for compatibility with older versions 74 of Org." 75 (declare (indent 2)) 76 (setq fun (if (functionp fun) fun `(lambda (node) ,fun))) 77 (let ((up (if with-self datum (org-element-parent datum))) 78 acc rtn) 79 (catch :--first-match 80 (while up 81 (when (or (not types) (org-element-type-p up types)) 82 (setq rtn (funcall fun up)) 83 (if (and first-match rtn) 84 (throw :--first-match rtn) 85 (when rtn (push rtn acc)))) 86 (setq up (org-element-parent up))) 87 (nreverse acc))))) 88 (if (fboundp 'org-element-begin) 89 (progn (declare-function org-element-begin "org-element") 90 (defalias 'gptel-org--element-begin 'org-element-begin)) 91 (defun gptel-org--element-begin (node) 92 "Get `:begin' property of NODE." 93 (org-element-property :begin node)))) 94 95 96 ;;; User options 97 (defcustom gptel-org-branching-context nil 98 "Use the lineage of the current heading as the context for gptel in Org buffers. 99 100 This makes each same level heading a separate conversation 101 branch. 102 103 By default, gptel uses a linear context: all the text up to the 104 cursor is sent to the LLM. Enabling this option makes the 105 context the hierarchical lineage of the current Org heading. In 106 this example: 107 108 ----- 109 Top level text 110 111 * Heading 1 112 heading 1 text 113 114 * Heading 2 115 heading 2 text 116 117 ** Heading 2.1 118 heading 2.1 text 119 ** Heading 2.2 120 heading 2.2 text 121 ----- 122 123 With the cursor at the end of the buffer, the text sent to the 124 LLM will be limited to 125 126 ----- 127 Top level text 128 129 * Heading 2 130 heading 2 text 131 132 ** Heading 2.2 133 heading 2.2 text 134 ----- 135 136 This makes it feasible to have multiple conversation branches." 137 :local t 138 :type 'boolean 139 :group 'gptel) 140 141 142 ;;; Setting context and creating queries 143 (defun gptel-org--get-topic-start () 144 "If a conversation topic is set, return it." 145 (when (org-entry-get (point) "GPTEL_TOPIC" 'inherit) 146 (marker-position org-entry-property-inherited-from))) 147 148 (defun gptel-org-set-topic (topic) 149 "Set a TOPIC and limit this conversation to the current heading. 150 151 This limits the context sent to the LLM to the text between the 152 current heading and the cursor position." 153 (interactive 154 (list 155 (progn 156 (or (derived-mode-p 'org-mode) 157 (user-error "Support for multiple topics per buffer is only implemented for `org-mode'")) 158 (completing-read "Set topic as: " 159 (org-property-values "GPTEL_TOPIC") 160 nil nil (downcase 161 (truncate-string-to-width 162 (substring-no-properties 163 (replace-regexp-in-string 164 "\\s-+" "-" 165 (org-get-heading))) 166 50)))))) 167 (when (stringp topic) (org-set-property "GPTEL_TOPIC" topic))) 168 169 ;; NOTE: This can be converted to a cl-defmethod for `gptel--parse-buffer' 170 ;; (conceptually cleaner), but will cause load-order issues in gptel.el and 171 ;; might be harder to debug. 172 (defun gptel-org--create-prompt (&optional prompt-end) 173 "Return a full conversation prompt from the contents of this Org buffer. 174 175 If `gptel--num-messages-to-send' is set, limit to that many 176 recent exchanges. 177 178 The prompt is constructed from the contents of the buffer up to 179 point, or PROMPT-END if provided. Its contents depend on the 180 value of `gptel-org-branching-context', which see." 181 (unless prompt-end (setq prompt-end (point))) 182 (let ((max-entries (and gptel--num-messages-to-send 183 (* 2 gptel--num-messages-to-send))) 184 (topic-start (gptel-org--get-topic-start))) 185 (when topic-start 186 ;; narrow to GPTEL_TOPIC property scope 187 (narrow-to-region topic-start prompt-end)) 188 (if gptel-org-branching-context 189 ;; Create prompt from direct ancestors of point 190 (if (fboundp 'org-element-lineage-map) 191 (save-excursion 192 (let* ((org-buf (current-buffer)) 193 (start-bounds (gptel-org--element-lineage-map 194 (org-element-at-point) #'gptel-org--element-begin 195 '(headline org-data) 'with-self)) 196 (end-bounds 197 (cl-loop 198 for (pos . rest) on (cdr start-bounds) 199 while 200 (and (>= pos (point-min)) ;respect narrowing 201 (goto-char pos) 202 ;; org-element-lineage always returns an extra 203 ;; (org-data) element at point 1. If there is also a 204 ;; heading here, it is either a false positive or we 205 ;; would be double counting it. So we reject this node 206 ;; when also at a heading. 207 (not (and (eq pos 1) (org-at-heading-p) 208 ;; Skip if at the last element of start-bounds, 209 ;; since we captured this heading already (#476) 210 (null rest)))) 211 do (outline-next-heading) 212 collect (point) into ends 213 finally return (cons prompt-end ends)))) 214 (with-temp-buffer 215 (setq-local gptel-backend (buffer-local-value 'gptel-backend org-buf) 216 gptel--system-message 217 (buffer-local-value 'gptel--system-message org-buf) 218 gptel-model (buffer-local-value 'gptel-model org-buf) 219 gptel-mode (buffer-local-value 'gptel-mode org-buf) 220 gptel-track-response 221 (buffer-local-value 'gptel-track-response org-buf) 222 gptel-track-media 223 (buffer-local-value 'gptel-track-media org-buf)) 224 (cl-loop for start in start-bounds 225 for end in end-bounds 226 do (insert-buffer-substring org-buf start end) 227 (goto-char (point-min))) 228 (goto-char (point-max)) 229 (let ((major-mode 'org-mode)) 230 (gptel--parse-buffer gptel-backend max-entries))))) 231 (display-warning 232 '(gptel org) 233 "Using `gptel-org-branching-context' requires Org version 9.6.7 or higher, it will be ignored.") 234 (gptel--parse-buffer gptel-backend max-entries)) 235 ;; Create prompt the usual way 236 (gptel--parse-buffer gptel-backend max-entries)))) 237 238 ;; Handle media links in the buffer 239 (cl-defmethod gptel--parse-media-links ((_mode (eql 'org-mode)) beg end) 240 "Parse text and actionable links between BEG and END. 241 242 Return a list of the form 243 ((:text \"some text\") 244 (:media \"/path/to/media.png\" :mime \"image/png\") 245 (:text \"More text\")) 246 for inclusion into the user prompt for the gptel request." 247 (require 'mailcap) ;FIXME Avoid this somehow 248 (let ((parts) (from-pt) 249 (link-regex (concat "\\(?:" org-link-bracket-re "\\|" 250 org-link-angle-re "\\)"))) 251 (save-excursion 252 (setq from-pt (goto-char beg)) 253 (while (re-search-forward link-regex end t) 254 (when-let* ((link (org-element-context)) 255 ((gptel-org--link-standalone-p link)) 256 (raw-link (org-element-property :raw-link link)) 257 (path (org-element-property :path link)) 258 (type (org-element-property :type link)) 259 ;; FIXME This is not a good place to check for url capability! 260 ((member type `("attachment" "file" 261 ,@(and (gptel--model-capable-p 'url) 262 '("http" "https" "ftp"))))) 263 (mime (mailcap-file-name-to-mime-type path)) 264 ((gptel--model-mime-capable-p mime))) 265 (cond 266 ((member type '("file" "attachment")) 267 (when (file-readable-p path) 268 ;; Collect text up to this image, and 269 ;; Collect this image 270 (when-let ((text (string-trim (buffer-substring-no-properties 271 from-pt (gptel-org--element-begin link))))) 272 (unless (string-empty-p text) (push (list :text text) parts))) 273 (push (list :media path :mime mime) parts) 274 (setq from-pt (point)))) 275 ((member type '("http" "https" "ftp")) 276 ;; Collect text up to this image, and 277 ;; Collect this image url 278 (when-let ((text (string-trim (buffer-substring-no-properties 279 from-pt (gptel-org--element-begin link))))) 280 (unless (string-empty-p text) (push (list :text text) parts))) 281 (push (list :url raw-link :mime mime) parts) 282 (setq from-pt (point)))))) 283 (unless (= from-pt end) 284 (push (list :text (buffer-substring-no-properties from-pt end)) parts))) 285 (nreverse parts))) 286 287 (defun gptel-org--link-standalone-p (object) 288 "Check if link OBJECT is on a line by itself." 289 ;; Specify ancestor TYPES as list (#245) 290 (let ((par (org-element-lineage object '(paragraph)))) 291 (and (= (gptel-org--element-begin object) 292 (save-excursion 293 (goto-char (org-element-property :contents-begin par)) 294 (skip-chars-forward "\t ") 295 (point))) ;account for leading space 296 ;before object 297 (<= (- (org-element-property :contents-end par) 298 (org-element-property :end object)) 299 1)))) 300 301 (defun gptel-org--send-with-props (send-fun &rest args) 302 "Conditionally modify SEND-FUN's calling environment. 303 304 If in an Org buffer under a heading containing a stored gptel 305 configuration, use that for requests instead. This includes the 306 system message, model and provider (backend), among other 307 parameters. 308 309 ARGS are the original function call arguments." 310 (if (derived-mode-p 'org-mode) 311 (pcase-let ((`(,gptel--system-message ,gptel-backend ,gptel-model 312 ,gptel-temperature ,gptel-max-tokens) 313 (seq-mapn (lambda (a b) (or a b)) 314 (gptel-org--entry-properties) 315 (list gptel--system-message gptel-backend gptel-model 316 gptel-temperature gptel-max-tokens)))) 317 (apply send-fun args)) 318 (apply send-fun args))) 319 320 (advice-add 'gptel-send :around #'gptel-org--send-with-props) 321 (advice-add 'gptel--suffix-send :around #'gptel-org--send-with-props) 322 323 ;; ;; NOTE: Basic uses in org-mode are covered by advising gptel-send and 324 ;; ;; gptel--suffix-send. For custom commands it might be necessary to advise 325 ;; ;; gptel-request instead. 326 ;; (advice-add 'gptel-request :around #'gptel-org--send-with-props) 327 328 329 ;;; Saving and restoring state 330 (defun gptel-org--entry-properties (&optional pt) 331 "Find gptel configuration properties stored at PT." 332 (pcase-let 333 ((`(,system ,backend ,model ,temperature ,tokens ,num) 334 (mapcar 335 (lambda (prop) (org-entry-get (or pt (point)) prop 'selective)) 336 '("GPTEL_SYSTEM" "GPTEL_BACKEND" "GPTEL_MODEL" 337 "GPTEL_TEMPERATURE" "GPTEL_MAX_TOKENS" 338 "GPTEL_NUM_MESSAGES_TO_SEND")))) 339 (when system 340 (setq system (string-replace "\\n" "\n" system))) 341 (when backend 342 (setq backend (alist-get backend gptel--known-backends 343 nil nil #'equal))) 344 (when model (setq model (gptel--intern model))) 345 (when temperature 346 (setq temperature (gptel--to-number temperature))) 347 (when tokens (setq tokens (gptel--to-number tokens))) 348 (when num (setq num (gptel--to-number num))) 349 (list system backend model temperature tokens num))) 350 351 (defun gptel-org--restore-state () 352 "Restore gptel state for Org buffers when turning on `gptel-mode'." 353 (save-restriction 354 (widen) 355 (condition-case status 356 (progn 357 (when-let ((bounds (org-entry-get (point-min) "GPTEL_BOUNDS"))) 358 (mapc (pcase-lambda (`(,beg . ,end)) 359 (put-text-property beg end 'gptel 'response)) 360 (read bounds))) 361 (pcase-let ((`(,system ,backend ,model ,temperature ,tokens ,num) 362 (gptel-org--entry-properties (point-min)))) 363 (when system (setq-local gptel--system-message system)) 364 (if backend (setq-local gptel-backend backend) 365 (message 366 (substitute-command-keys 367 (concat 368 "Could not activate gptel backend \"%s\"! " 369 "Switch backends with \\[universal-argument] \\[gptel-send]" 370 " before using gptel.")) 371 backend)) 372 (when model (setq-local gptel-model model)) 373 (when temperature (setq-local gptel-temperature temperature)) 374 (when tokens (setq-local gptel-max-tokens tokens)) 375 (when num (setq-local gptel--num-messages-to-send num)))) 376 (:success (message "gptel chat restored.")) 377 (error (message "Could not restore gptel state, sorry! Error: %s" status))))) 378 379 (defun gptel-org-set-properties (pt &optional msg) 380 "Store the active gptel configuration under the current heading. 381 382 The active gptel configuration includes the current system 383 message, language model and provider (backend), and additional 384 settings when applicable. 385 386 PT is the cursor position by default. If MSG is 387 non-nil (default), display a message afterwards." 388 (interactive (list (point) t)) 389 (org-entry-put pt "GPTEL_MODEL" (gptel--model-name gptel-model)) 390 (org-entry-put pt "GPTEL_BACKEND" (gptel-backend-name gptel-backend)) 391 (unless (equal (default-value 'gptel-temperature) gptel-temperature) 392 (org-entry-put pt "GPTEL_TEMPERATURE" 393 (number-to-string gptel-temperature))) 394 (when (natnump gptel--num-messages-to-send) 395 (org-entry-put pt "GPTEL_NUM_MESSAGES_TO_SEND" 396 (number-to-string gptel--num-messages-to-send))) 397 (org-entry-put pt "GPTEL_SYSTEM" 398 (string-replace "\n" "\\n" gptel--system-message)) 399 (when gptel-max-tokens 400 (org-entry-put 401 pt "GPTEL_MAX_TOKENS" (number-to-string gptel-max-tokens))) 402 (when msg 403 (message "Added gptel configuration to current headline."))) 404 405 (defun gptel-org--save-state () 406 "Write the gptel state to the Org buffer as Org properties." 407 (org-with-wide-buffer 408 (goto-char (point-min)) 409 (when (org-at-heading-p) 410 (org-open-line 1)) 411 (gptel-org-set-properties (point-min)) 412 ;; Save response boundaries 413 (letrec ((write-bounds 414 (lambda (attempts) 415 (let* ((bounds (gptel--get-buffer-bounds)) 416 (offset (caar bounds)) 417 (offset-marker (set-marker (make-marker) offset))) 418 (org-entry-put (point-min) "GPTEL_BOUNDS" 419 (prin1-to-string (gptel--get-buffer-bounds))) 420 (when (and (not (= (marker-position offset-marker) offset)) 421 (> attempts 0)) 422 (funcall write-bounds (1- attempts))))))) 423 (funcall write-bounds 6)))) 424 425 426 ;;; Transforming responses 427 (defun gptel--convert-markdown->org (str) 428 "Convert string STR from markdown to org markup. 429 430 This is a very basic converter that handles only a few markup 431 elements." 432 (interactive) 433 (with-temp-buffer 434 (insert str) 435 (goto-char (point-min)) 436 (while (re-search-forward "`+\\|\\*\\{1,2\\}\\|_\\|^#+" nil t) 437 (pcase (match-string 0) 438 ;; Handle backticks 439 ((and (guard (eq (char-before) ?`)) ticks) 440 (gptel--replace-source-marker (length ticks)) 441 (save-match-data 442 (catch 'block-end 443 (while (search-forward ticks nil t) 444 (unless (or (eq (char-before (match-beginning 0)) ?`) 445 (eq (char-after) ?`)) 446 (gptel--replace-source-marker (length ticks) 'end) 447 (throw 'block-end nil)))))) 448 ;; Handle headings 449 ((and (guard (eq (char-before) ?#)) heading) 450 (when (looking-at "[[:space:]]") 451 (delete-region (line-beginning-position) (point)) 452 (insert (make-string (length heading) ?*)))) 453 ;; Handle emphasis 454 ("**" (cond 455 ;; ((looking-at "\\*\\(?:[[:word:]]\\|\s\\)") 456 ;; (delete-char 1)) 457 ((looking-back "\\(?:[[:word:][:punct:]\n]\\|\s\\)\\*\\{2\\}" 458 (max (- (point) 3) (point-min))) 459 (delete-char -1)))) 460 ("*" 461 (cond 462 ((save-match-data 463 (and (looking-back "\\(?:[[:space:]]\\|\s\\)\\(?:_\\|\\*\\)" 464 (max (- (point) 2) (point-min))) 465 (not (looking-at "[[:space:]]\\|\s")))) 466 ;; Possible beginning of emphasis 467 (and 468 (save-excursion 469 (when (and (re-search-forward (regexp-quote (match-string 0)) 470 (line-end-position) t) 471 (looking-at "[[:space]]\\|\s") 472 (not (looking-back "\\(?:[[:space]]\\|\s\\)\\(?:_\\|\\*\\)" 473 (max (- (point) 2) (point-min))))) 474 (delete-char -1) (insert "/") t)) 475 (progn (delete-char -1) (insert "/")))) 476 ((save-excursion 477 (ignore-errors (backward-char 2)) 478 (looking-at "\\(?:$\\|\\`\\)\n\\*[[:space:]]")) 479 ;; Bullet point, replace with hyphen 480 (delete-char -1) (insert "-")))))) 481 (buffer-string))) 482 483 (defun gptel--replace-source-marker (num-ticks &optional end) 484 "Replace markdown style backticks with Org equivalents. 485 486 NUM-TICKS is the number of backticks being replaced. If END is 487 true these are \"ending\" backticks. 488 489 This is intended for use in the markdown to org stream converter." 490 (let ((from (match-beginning 0))) 491 (delete-region from (point)) 492 (if (and (= num-ticks 3) 493 (save-excursion (beginning-of-line) 494 (skip-chars-forward " \t") 495 (eq (point) from))) 496 (insert (if end "#+end_src" "#+begin_src ")) 497 (insert "=")))) 498 499 (defun gptel--stream-convert-markdown->org () 500 "Return a Markdown to Org converter. 501 502 This function parses a stream of Markdown text to Org 503 continuously when it is called with successive chunks of the 504 text stream." 505 (letrec ((in-src-block nil) ;explicit nil to address BUG #183 506 (temp-buf (generate-new-buffer-name "*gptel-temp*")) 507 (start-pt (make-marker)) 508 (ticks-total 0) 509 (cleanup-fn 510 (lambda (&rest _) 511 (when (buffer-live-p (get-buffer temp-buf)) 512 (set-marker start-pt nil) 513 (kill-buffer temp-buf)) 514 (remove-hook 'gptel-post-response-functions cleanup-fn)))) 515 (add-hook 'gptel-post-response-functions cleanup-fn) 516 (lambda (str) 517 (let ((noop-p) (ticks 0)) 518 (with-current-buffer (get-buffer-create temp-buf) 519 (save-excursion (goto-char (point-max)) (insert str)) 520 (when (marker-position start-pt) (goto-char start-pt)) 521 (when in-src-block (setq ticks ticks-total)) 522 (save-excursion 523 (while (re-search-forward "`\\|\\*\\{1,2\\}\\|_\\|^#+" nil t) 524 (pcase (match-string 0) 525 ("`" 526 ;; Count number of consecutive backticks 527 (backward-char) 528 (while (and (char-after) (eq (char-after) ?`)) 529 (forward-char) 530 (if in-src-block (cl-decf ticks) (cl-incf ticks))) 531 ;; Set the verbatim state of the parser 532 (if (and (eobp) 533 ;; Special case heuristic: If the response ends with 534 ;; ^``` we don't wait for more input. 535 ;; FIXME: This can have false positives. 536 (not (save-excursion (beginning-of-line) 537 (looking-at "^```$")))) 538 ;; End of input => there could be more backticks coming, 539 ;; so we wait for more input 540 (progn (setq noop-p t) (set-marker start-pt (match-beginning 0))) 541 ;; We reached a character other than a backtick 542 (cond 543 ;; Ticks balanced, end src block 544 ((= ticks 0) 545 (progn (setq in-src-block nil) 546 (gptel--replace-source-marker ticks-total 'end))) 547 ;; Positive number of ticks, start an src block 548 ((and (> ticks 0) (not in-src-block)) 549 (setq ticks-total ticks 550 in-src-block t) 551 (gptel--replace-source-marker ticks-total)) 552 ;; Negative number of ticks or in a src block already, 553 ;; reset ticks 554 (t (setq ticks ticks-total))))) 555 ;; Handle other chars: heading, emphasis, bold and bullet items 556 ((and (guard (and (not in-src-block) (eq (char-before) ?#))) heading) 557 (if (eobp) 558 ;; Not enough information about the heading yet 559 (progn (setq noop-p t) (set-marker start-pt (match-beginning 0))) 560 ;; Convert markdown heading to Org heading 561 (when (looking-at "[[:space:]]") 562 (delete-region (line-beginning-position) (point)) 563 (insert (make-string (length heading) ?*))))) 564 ((and "**" (guard (not in-src-block))) 565 (cond 566 ;; TODO Not sure why this branch was needed 567 ;; ((looking-at "\\*\\(?:[[:word:]]\\|\s\\)") (delete-char 1)) 568 569 ;; Looking back at "w**" or " **" 570 ((looking-back "\\(?:[[:word:][:punct:]\n]\\|\s\\)\\*\\{2\\}" 571 (max (- (point) 3) (point-min))) 572 (delete-char -1)))) 573 ((and "*" (guard (not in-src-block))) 574 (if (eobp) 575 ;; Not enough information about the "*" yet 576 (progn (setq noop-p t) (set-marker start-pt (match-beginning 0))) 577 ;; "*" is either emphasis or a bullet point 578 (save-match-data 579 (save-excursion 580 (ignore-errors (backward-char 2)) 581 (cond 582 ((or (looking-at 583 "[^[:space:][:punct:]\n]\\(?:_\\|\\*\\)\\(?:[[:space:][:punct:]]\\|$\\)") 584 (looking-at 585 "\\(?:[[:space:][:punct:]]\\)\\(?:_\\|\\*\\)\\([^[:space:][:punct:]]\\|$\\)")) 586 ;; Emphasis, replace with slashes 587 (forward-char 2) (delete-char -1) (insert "/")) 588 ((looking-at "\\(?:$\\|\\`\\)\n\\*[[:space:]]") 589 ;; Bullet point, replace with hyphen 590 (forward-char 2) (delete-char -1) (insert "-")))))))))) 591 (if noop-p 592 (buffer-substring (point) start-pt) 593 (prog1 (buffer-substring (point) (point-max)) 594 (set-marker start-pt (point-max))))))))) 595 596 (provide 'gptel-org) 597 ;;; gptel-org.el ends here