config

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

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