config

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

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