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