config

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

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