gptel-org.el (26295B)
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 in (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 do (outline-next-heading) 209 collect (point) into ends 210 finally return (cons prompt-end ends)))) 211 (with-temp-buffer 212 (setq-local gptel-backend (buffer-local-value 'gptel-backend org-buf) 213 gptel--system-message 214 (buffer-local-value 'gptel--system-message org-buf) 215 gptel-model (buffer-local-value 'gptel-model org-buf) 216 gptel-mode (buffer-local-value 'gptel-mode org-buf) 217 gptel-track-response 218 (buffer-local-value 'gptel-track-response org-buf) 219 gptel-track-media 220 (buffer-local-value 'gptel-track-media org-buf)) 221 (cl-loop for start in start-bounds 222 for end in end-bounds 223 do (insert-buffer-substring org-buf start end) 224 (goto-char (point-min))) 225 (goto-char (point-max)) 226 (let ((major-mode 'org-mode)) 227 (gptel--parse-buffer gptel-backend max-entries))))) 228 (display-warning 229 '(gptel org) 230 "Using `gptel-org-branching-context' requires Org version 9.6.7 or higher, it will be ignored.") 231 (gptel--parse-buffer gptel-backend max-entries)) 232 ;; Create prompt the usual way 233 (gptel--parse-buffer gptel-backend max-entries)))) 234 235 ;; Handle media links in the buffer 236 (cl-defmethod gptel--parse-media-links ((_mode (eql 'org-mode)) beg end) 237 "Parse text and actionable links between BEG and END. 238 239 Return a list of the form 240 ((:text \"some text\") 241 (:media \"/path/to/media.png\" :mime \"image/png\") 242 (:text \"More text\")) 243 for inclusion into the user prompt for the gptel request." 244 (require 'mailcap) ;FIXME Avoid this somehow 245 (let ((parts) (from-pt) 246 (link-regex (concat "\\(?:" org-link-bracket-re "\\|" 247 org-link-angle-re "\\)"))) 248 (save-excursion 249 (setq from-pt (goto-char beg)) 250 (while (re-search-forward link-regex end t) 251 (when-let* ((link (org-element-context)) 252 ((gptel-org--link-standalone-p link)) 253 (raw-link (org-element-property :raw-link link)) 254 (path (org-element-property :path link)) 255 (type (org-element-property :type link)) 256 ;; FIXME This is not a good place to check for url capability! 257 ((member type `("attachment" "file" 258 ,@(and (gptel--model-capable-p 'url) 259 '("http" "https" "ftp"))))) 260 (mime (mailcap-file-name-to-mime-type path)) 261 ((gptel--model-mime-capable-p mime))) 262 (cond 263 ((member type '("file" "attachment")) 264 (when (file-readable-p path) 265 ;; Collect text up to this image, and 266 ;; Collect this image 267 (when-let ((text (string-trim (buffer-substring-no-properties 268 from-pt (gptel-org--element-begin link))))) 269 (unless (string-empty-p text) (push (list :text text) parts))) 270 (push (list :media path :mime mime) parts) 271 (setq from-pt (point)))) 272 ((member type '("http" "https" "ftp")) 273 ;; Collect text up to this image, and 274 ;; Collect this image url 275 (when-let ((text (string-trim (buffer-substring-no-properties 276 from-pt (gptel-org--element-begin link))))) 277 (unless (string-empty-p text) (push (list :text text) parts))) 278 (push (list :url raw-link :mime mime) parts) 279 (setq from-pt (point)))))) 280 (unless (= from-pt end) 281 (push (list :text (buffer-substring-no-properties from-pt end)) parts))) 282 (nreverse parts))) 283 284 (defun gptel-org--link-standalone-p (object) 285 "Check if link OBJECT is on a line by itself." 286 ;; Specify ancestor TYPES as list (#245) 287 (let ((par (org-element-lineage object '(paragraph)))) 288 (and (= (gptel-org--element-begin object) 289 (save-excursion 290 (goto-char (org-element-property :contents-begin par)) 291 (skip-chars-forward "\t ") 292 (point))) ;account for leading space 293 ;before object 294 (<= (- (org-element-property :contents-end par) 295 (org-element-property :end object)) 296 1)))) 297 298 (defun gptel-org--send-with-props (send-fun &rest args) 299 "Conditionally modify SEND-FUN's calling environment. 300 301 If in an Org buffer under a heading containing a stored gptel 302 configuration, use that for requests instead. This includes the 303 system message, model and provider (backend), among other 304 parameters. 305 306 ARGS are the original function call arguments." 307 (if (derived-mode-p 'org-mode) 308 (pcase-let ((`(,gptel--system-message ,gptel-backend ,gptel-model 309 ,gptel-temperature ,gptel-max-tokens) 310 (seq-mapn (lambda (a b) (or a b)) 311 (gptel-org--entry-properties) 312 (list gptel--system-message gptel-backend gptel-model 313 gptel-temperature gptel-max-tokens)))) 314 (apply send-fun args)) 315 (apply send-fun args))) 316 317 (advice-add 'gptel-send :around #'gptel-org--send-with-props) 318 (advice-add 'gptel--suffix-send :around #'gptel-org--send-with-props) 319 320 ;; ;; NOTE: Basic uses in org-mode are covered by advising gptel-send and 321 ;; ;; gptel--suffix-send. For custom commands it might be necessary to advise 322 ;; ;; gptel-request instead. 323 ;; (advice-add 'gptel-request :around #'gptel-org--send-with-props) 324 325 326 ;;; Saving and restoring state 327 (defun gptel-org--entry-properties (&optional pt) 328 "Find gptel configuration properties stored at PT." 329 (pcase-let 330 ((`(,system ,backend ,model ,temperature ,tokens ,num) 331 (mapcar 332 (lambda (prop) (org-entry-get (or pt (point)) prop 'selective)) 333 '("GPTEL_SYSTEM" "GPTEL_BACKEND" "GPTEL_MODEL" 334 "GPTEL_TEMPERATURE" "GPTEL_MAX_TOKENS" 335 "GPTEL_NUM_MESSAGES_TO_SEND")))) 336 (when system 337 (setq system (string-replace "\\n" "\n" system))) 338 (when backend 339 (setq backend (alist-get backend gptel--known-backends 340 nil nil #'equal))) 341 (when model (setq model (gptel--intern model))) 342 (when temperature 343 (setq temperature (gptel--to-number temperature))) 344 (when tokens (setq tokens (gptel--to-number tokens))) 345 (when num (setq num (gptel--to-number num))) 346 (list system backend model temperature tokens num))) 347 348 (defun gptel-org--restore-state () 349 "Restore gptel state for Org buffers when turning on `gptel-mode'." 350 (save-restriction 351 (widen) 352 (condition-case status 353 (progn 354 (when-let ((bounds (org-entry-get (point-min) "GPTEL_BOUNDS"))) 355 (mapc (pcase-lambda (`(,beg . ,end)) 356 (put-text-property beg end 'gptel 'response)) 357 (read bounds))) 358 (pcase-let ((`(,system ,backend ,model ,temperature ,tokens ,num) 359 (gptel-org--entry-properties (point-min)))) 360 (when system (setq-local gptel--system-message system)) 361 (if backend (setq-local gptel-backend backend) 362 (message 363 (substitute-command-keys 364 (concat 365 "Could not activate gptel backend \"%s\"! " 366 "Switch backends with \\[universal-argument] \\[gptel-send]" 367 " before using gptel.")) 368 backend)) 369 (when model (setq-local gptel-model model)) 370 (when temperature (setq-local gptel-temperature temperature)) 371 (when tokens (setq-local gptel-max-tokens tokens)) 372 (when num (setq-local gptel--num-messages-to-send num)))) 373 (:success (message "gptel chat restored.")) 374 (error (message "Could not restore gptel state, sorry! Error: %s" status))))) 375 376 (defun gptel-org-set-properties (pt &optional msg) 377 "Store the active gptel configuration under the current heading. 378 379 The active gptel configuration includes the current system 380 message, language model and provider (backend), and additional 381 settings when applicable. 382 383 PT is the cursor position by default. If MSG is 384 non-nil (default), display a message afterwards." 385 (interactive (list (point) t)) 386 (org-entry-put pt "GPTEL_MODEL" (gptel--model-name gptel-model)) 387 (org-entry-put pt "GPTEL_BACKEND" (gptel-backend-name gptel-backend)) 388 (unless (equal (default-value 'gptel-temperature) gptel-temperature) 389 (org-entry-put pt "GPTEL_TEMPERATURE" 390 (number-to-string gptel-temperature))) 391 (when (natnump gptel--num-messages-to-send) 392 (org-entry-put pt "GPTEL_NUM_MESSAGES_TO_SEND" 393 (number-to-string gptel--num-messages-to-send))) 394 (org-entry-put pt "GPTEL_SYSTEM" 395 (string-replace "\n" "\\n" gptel--system-message)) 396 (when gptel-max-tokens 397 (org-entry-put 398 pt "GPTEL_MAX_TOKENS" (number-to-string gptel-max-tokens))) 399 (when msg 400 (message "Added gptel configuration to current headline."))) 401 402 (defun gptel-org--save-state () 403 "Write the gptel state to the Org buffer as Org properties." 404 (org-with-wide-buffer 405 (goto-char (point-min)) 406 (when (org-at-heading-p) 407 (org-open-line 1)) 408 (gptel-org-set-properties (point-min)) 409 ;; Save response boundaries 410 (letrec ((write-bounds 411 (lambda (attempts) 412 (let* ((bounds (gptel--get-buffer-bounds)) 413 (offset (caar bounds)) 414 (offset-marker (set-marker (make-marker) offset))) 415 (org-entry-put (point-min) "GPTEL_BOUNDS" 416 (prin1-to-string (gptel--get-buffer-bounds))) 417 (when (and (not (= (marker-position offset-marker) offset)) 418 (> attempts 0)) 419 (funcall write-bounds (1- attempts))))))) 420 (funcall write-bounds 6)))) 421 422 423 ;;; Transforming responses 424 (defun gptel--convert-markdown->org (str) 425 "Convert string STR from markdown to org markup. 426 427 This is a very basic converter that handles only a few markup 428 elements." 429 (interactive) 430 (with-temp-buffer 431 (insert str) 432 (goto-char (point-min)) 433 (while (re-search-forward "`+\\|\\*\\{1,2\\}\\|_\\|^#+" nil t) 434 (pcase (match-string 0) 435 ;; Handle backticks 436 ((and (guard (eq (char-before) ?`)) ticks) 437 (gptel--replace-source-marker (length ticks)) 438 (save-match-data 439 (catch 'block-end 440 (while (search-forward ticks nil t) 441 (unless (or (eq (char-before (match-beginning 0)) ?`) 442 (eq (char-after) ?`)) 443 (gptel--replace-source-marker (length ticks) 'end) 444 (throw 'block-end nil)))))) 445 ;; Handle headings 446 ((and (guard (eq (char-before) ?#)) heading) 447 (when (looking-at "[[:space:]]") 448 (delete-region (line-beginning-position) (point)) 449 (insert (make-string (length heading) ?*)))) 450 ;; Handle emphasis 451 ("**" (cond 452 ;; ((looking-at "\\*\\(?:[[:word:]]\\|\s\\)") 453 ;; (delete-char 1)) 454 ((looking-back "\\(?:[[:word:][:punct:]\n]\\|\s\\)\\*\\{2\\}" 455 (max (- (point) 3) (point-min))) 456 (delete-char -1)))) 457 ("*" 458 (cond 459 ((save-match-data 460 (and (looking-back "\\(?:[[:space:]]\\|\s\\)\\(?:_\\|\\*\\)" 461 (max (- (point) 2) (point-min))) 462 (not (looking-at "[[:space:]]\\|\s")))) 463 ;; Possible beginning of emphasis 464 (and 465 (save-excursion 466 (when (and (re-search-forward (regexp-quote (match-string 0)) 467 (line-end-position) t) 468 (looking-at "[[:space]]\\|\s") 469 (not (looking-back "\\(?:[[:space]]\\|\s\\)\\(?:_\\|\\*\\)" 470 (max (- (point) 2) (point-min))))) 471 (delete-char -1) (insert "/") t)) 472 (progn (delete-char -1) (insert "/")))) 473 ((save-excursion 474 (ignore-errors (backward-char 2)) 475 (looking-at "\\(?:$\\|\\`\\)\n\\*[[:space:]]")) 476 ;; Bullet point, replace with hyphen 477 (delete-char -1) (insert "-")))))) 478 (buffer-string))) 479 480 (defun gptel--replace-source-marker (num-ticks &optional end) 481 "Replace markdown style backticks with Org equivalents. 482 483 NUM-TICKS is the number of backticks being replaced. If END is 484 true these are \"ending\" backticks. 485 486 This is intended for use in the markdown to org stream converter." 487 (let ((from (match-beginning 0))) 488 (delete-region from (point)) 489 (if (and (= num-ticks 3) 490 (save-excursion (beginning-of-line) 491 (skip-chars-forward " \t") 492 (eq (point) from))) 493 (insert (if end "#+end_src" "#+begin_src ")) 494 (insert "=")))) 495 496 (defun gptel--stream-convert-markdown->org () 497 "Return a Markdown to Org converter. 498 499 This function parses a stream of Markdown text to Org 500 continuously when it is called with successive chunks of the 501 text stream." 502 (letrec ((in-src-block nil) ;explicit nil to address BUG #183 503 (temp-buf (generate-new-buffer-name "*gptel-temp*")) 504 (start-pt (make-marker)) 505 (ticks-total 0) 506 (cleanup-fn 507 (lambda (&rest _) 508 (when (buffer-live-p (get-buffer temp-buf)) 509 (set-marker start-pt nil) 510 (kill-buffer temp-buf)) 511 (remove-hook 'gptel-post-response-functions cleanup-fn)))) 512 (add-hook 'gptel-post-response-functions cleanup-fn) 513 (lambda (str) 514 (let ((noop-p) (ticks 0)) 515 (with-current-buffer (get-buffer-create temp-buf) 516 (save-excursion (goto-char (point-max)) (insert str)) 517 (when (marker-position start-pt) (goto-char start-pt)) 518 (when in-src-block (setq ticks ticks-total)) 519 (save-excursion 520 (while (re-search-forward "`\\|\\*\\{1,2\\}\\|_\\|^#+" nil t) 521 (pcase (match-string 0) 522 ("`" 523 ;; Count number of consecutive backticks 524 (backward-char) 525 (while (and (char-after) (eq (char-after) ?`)) 526 (forward-char) 527 (if in-src-block (cl-decf ticks) (cl-incf ticks))) 528 ;; Set the verbatim state of the parser 529 (if (and (eobp) 530 ;; Special case heuristic: If the response ends with 531 ;; ^``` we don't wait for more input. 532 ;; FIXME: This can have false positives. 533 (not (save-excursion (beginning-of-line) 534 (looking-at "^```$")))) 535 ;; End of input => there could be more backticks coming, 536 ;; so we wait for more input 537 (progn (setq noop-p t) (set-marker start-pt (match-beginning 0))) 538 ;; We reached a character other than a backtick 539 (cond 540 ;; Ticks balanced, end src block 541 ((= ticks 0) 542 (progn (setq in-src-block nil) 543 (gptel--replace-source-marker ticks-total 'end))) 544 ;; Positive number of ticks, start an src block 545 ((and (> ticks 0) (not in-src-block)) 546 (setq ticks-total ticks 547 in-src-block t) 548 (gptel--replace-source-marker ticks-total)) 549 ;; Negative number of ticks or in a src block already, 550 ;; reset ticks 551 (t (setq ticks ticks-total))))) 552 ;; Handle other chars: heading, emphasis, bold and bullet items 553 ((and (guard (and (not in-src-block) (eq (char-before) ?#))) heading) 554 (if (eobp) 555 ;; Not enough information about the heading yet 556 (progn (setq noop-p t) (set-marker start-pt (match-beginning 0))) 557 ;; Convert markdown heading to Org heading 558 (when (looking-at "[[:space:]]") 559 (delete-region (line-beginning-position) (point)) 560 (insert (make-string (length heading) ?*))))) 561 ((and "**" (guard (not in-src-block))) 562 (cond 563 ;; TODO Not sure why this branch was needed 564 ;; ((looking-at "\\*\\(?:[[:word:]]\\|\s\\)") (delete-char 1)) 565 566 ;; Looking back at "w**" or " **" 567 ((looking-back "\\(?:[[:word:][:punct:]\n]\\|\s\\)\\*\\{2\\}" 568 (max (- (point) 3) (point-min))) 569 (delete-char -1)))) 570 ((and "*" (guard (not in-src-block))) 571 (if (eobp) 572 ;; Not enough information about the "*" yet 573 (progn (setq noop-p t) (set-marker start-pt (match-beginning 0))) 574 ;; "*" is either emphasis or a bullet point 575 (save-match-data 576 (save-excursion 577 (ignore-errors (backward-char 2)) 578 (cond 579 ((or (looking-at 580 "[^[:space:][:punct:]\n]\\(?:_\\|\\*\\)\\(?:[[:space:][:punct:]]\\|$\\)") 581 (looking-at 582 "\\(?:[[:space:][:punct:]]\\)\\(?:_\\|\\*\\)\\([^[:space:][:punct:]]\\|$\\)")) 583 ;; Emphasis, replace with slashes 584 (forward-char 2) (delete-char -1) (insert "/")) 585 ((looking-at "\\(?:$\\|\\`\\)\n\\*[[:space:]]") 586 ;; Bullet point, replace with hyphen 587 (forward-char 2) (delete-char -1) (insert "-")))))))))) 588 (if noop-p 589 (buffer-substring (point) start-pt) 590 (prog1 (buffer-substring (point) (point-max)) 591 (set-marker start-pt (point-max))))))))) 592 593 (provide 'gptel-org) 594 ;;; gptel-org.el ends here